@rubytech/create-maxy-code 0.1.436 → 0.1.437

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 (207) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/lib/active-rules/dist/index.d.ts +14 -0
  3. package/payload/platform/lib/active-rules/dist/index.d.ts.map +1 -1
  4. package/payload/platform/lib/active-rules/dist/index.js +56 -9
  5. package/payload/platform/lib/active-rules/dist/index.js.map +1 -1
  6. package/payload/platform/lib/active-rules/src/index.test.ts +106 -0
  7. package/payload/platform/lib/active-rules/src/index.ts +98 -9
  8. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +6 -6
  9. package/payload/platform/plugins/docs/references/admin-session.md +2 -0
  10. package/payload/platform/plugins/docs/references/admin-ui.md +2 -2
  11. package/payload/platform/plugins/docs/references/internals.md +3 -3
  12. package/payload/platform/plugins/memory/mcp/dist/index.js +12 -3
  13. package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
  14. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-archive-write-progress.test.d.ts +2 -0
  15. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-archive-write-progress.test.d.ts.map +1 -0
  16. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-archive-write-progress.test.js +79 -0
  17. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-archive-write-progress.test.js.map +1 -0
  18. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest-write-phase-progress.test.d.ts +2 -0
  19. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest-write-phase-progress.test.d.ts.map +1 -0
  20. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest-write-phase-progress.test.js +50 -0
  21. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest-write-phase-progress.test.js.map +1 -0
  22. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-reindex-progress.test.d.ts +2 -0
  23. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-reindex-progress.test.d.ts.map +1 -0
  24. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-reindex-progress.test.js +114 -0
  25. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-reindex-progress.test.js.map +1 -0
  26. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-read-active-rules.test.js +5 -0
  27. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-read-active-rules.test.js.map +1 -1
  28. package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.d.ts +15 -0
  29. package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.d.ts.map +1 -1
  30. package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.js +17 -17
  31. package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.js.map +1 -1
  32. package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js +21 -1
  33. package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js.map +1 -1
  34. package/payload/platform/plugins/memory/mcp/dist/tools/memory-reindex.d.ts +1 -1
  35. package/payload/platform/plugins/memory/mcp/dist/tools/memory-reindex.d.ts.map +1 -1
  36. package/payload/platform/plugins/memory/mcp/dist/tools/memory-reindex.js +19 -4
  37. package/payload/platform/plugins/memory/mcp/dist/tools/memory-reindex.js.map +1 -1
  38. package/payload/platform/plugins/memory/mcp/dist/tools/profile-read.d.ts.map +1 -1
  39. package/payload/platform/plugins/memory/mcp/dist/tools/profile-read.js +4 -2
  40. package/payload/platform/plugins/memory/mcp/dist/tools/profile-read.js.map +1 -1
  41. package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
  42. package/payload/platform/services/claude-session-manager/dist/http-server.js +16 -6
  43. package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
  44. package/payload/platform/services/claude-session-manager/dist/jsonl-enumerator.d.ts +7 -0
  45. package/payload/platform/services/claude-session-manager/dist/jsonl-enumerator.d.ts.map +1 -1
  46. package/payload/platform/services/claude-session-manager/dist/jsonl-enumerator.js +9 -0
  47. package/payload/platform/services/claude-session-manager/dist/jsonl-enumerator.js.map +1 -1
  48. package/payload/platform/services/claude-session-manager/dist/preference-reconciliation-audit.d.ts +4 -1
  49. package/payload/platform/services/claude-session-manager/dist/preference-reconciliation-audit.d.ts.map +1 -1
  50. package/payload/platform/services/claude-session-manager/dist/preference-reconciliation-audit.js.map +1 -1
  51. package/payload/server/public/assets/{AdminLoginScreens-CMh1pdOp.js → AdminLoginScreens-DeR71H29.js} +1 -1
  52. package/payload/server/public/assets/AdminShell-DlAVQUda.js +2 -0
  53. package/payload/server/public/assets/{Checkbox-OCfZCTCO.js → Checkbox-B629mOPp.js} +1 -1
  54. package/payload/server/public/assets/{admin-Bi2uYnAg.js → admin-C15h-NL0.js} +1 -1
  55. package/payload/server/public/assets/{arc-Bm9oSOth.js → arc-qKfXIt8v.js} +1 -1
  56. package/payload/server/public/assets/architecture-YZFGNWBL-FPHT7s-C.js +1 -0
  57. package/payload/server/public/assets/{architectureDiagram-Q4EWVU46-C2dK88g3.js → architectureDiagram-Q4EWVU46-DkVrGn7G.js} +1 -1
  58. package/payload/server/public/assets/{blockDiagram-DXYQGD6D-DfLSsjCJ.js → blockDiagram-DXYQGD6D-DqavswGH.js} +1 -1
  59. package/payload/server/public/assets/browser-B7xQLIRK.js +1 -0
  60. package/payload/server/public/assets/{c4Diagram-AHTNJAMY-Bxkmp6OL.js → c4Diagram-AHTNJAMY-B7fyKiXe.js} +1 -1
  61. package/payload/server/public/assets/calendar-A-vGm6pL.js +1 -0
  62. package/payload/server/public/assets/channel-j70eMvcL.js +1 -0
  63. package/payload/server/public/assets/chat-DAae6Z51.js +1 -0
  64. package/payload/server/public/assets/chevron-left-u4tjq7fI.js +1 -0
  65. package/payload/server/public/assets/{chunk-2KRD3SAO-BVNDFwNH.js → chunk-2KRD3SAO-DsgMtGkv.js} +1 -1
  66. package/payload/server/public/assets/{chunk-336JU56O-Daz6nMta.js → chunk-336JU56O-CkdISfNt.js} +2 -2
  67. package/payload/server/public/assets/chunk-426QAEUC-D-NcAlTW.js +1 -0
  68. package/payload/server/public/assets/{chunk-4BX2VUAB-DTldfvwP.js → chunk-4BX2VUAB-CYpiF2bl.js} +1 -1
  69. package/payload/server/public/assets/{chunk-4TB4RGXK-DhM8pwRG.js → chunk-4TB4RGXK-CWyc_F7v.js} +1 -1
  70. package/payload/server/public/assets/{chunk-55IACEB6-CzTsbIHo.js → chunk-55IACEB6-BbzKMw2Z.js} +1 -1
  71. package/payload/server/public/assets/{chunk-5FUZZQ4R-BrcObZhe.js → chunk-5FUZZQ4R-u19Nai9e.js} +1 -1
  72. package/payload/server/public/assets/{chunk-5PVQY5BW-SFqTuS11.js → chunk-5PVQY5BW-Ba8by6Uy.js} +1 -1
  73. package/payload/server/public/assets/{chunk-67CJDMHE-EIbrSGOo.js → chunk-67CJDMHE-BwUuxtyb.js} +1 -1
  74. package/payload/server/public/assets/{chunk-7N4EOEYR-B1D4Jorr.js → chunk-7N4EOEYR-BRH9iM9L.js} +1 -1
  75. package/payload/server/public/assets/{chunk-AA7GKIK3-BKHiX4cC.js → chunk-AA7GKIK3-BGoztgLL.js} +1 -1
  76. package/payload/server/public/assets/{chunk-BSJP7CBP-xBzu29m7.js → chunk-BSJP7CBP-C5GDAZTl.js} +1 -1
  77. package/payload/server/public/assets/{chunk-CIAEETIT-x2x0gMpL.js → chunk-CIAEETIT-ChY32VPS.js} +1 -1
  78. package/payload/server/public/assets/{chunk-EDXVE4YY-D5UkD28M.js → chunk-EDXVE4YY-CPSD3p6p.js} +1 -1
  79. package/payload/server/public/assets/{chunk-ENJZ2VHE-Dbz67a7E.js → chunk-ENJZ2VHE-D1oL-iUr.js} +1 -1
  80. package/payload/server/public/assets/{chunk-FMBD7UC4-XfWvuu2F.js → chunk-FMBD7UC4-CPvYEr54.js} +1 -1
  81. package/payload/server/public/assets/{chunk-FOC6F5B3-DfOEDS4q.js → chunk-FOC6F5B3-9Yyj7Vbz.js} +1 -1
  82. package/payload/server/public/assets/{chunk-ICPOFSXX-D_WEZZvg.js → chunk-ICPOFSXX-BaYgh8FR.js} +2 -2
  83. package/payload/server/public/assets/{chunk-K5T4RW27-7O627Xi1.js → chunk-K5T4RW27-5ucpeRvm.js} +1 -1
  84. package/payload/server/public/assets/{chunk-KGLVRYIC-BXI3XDP9.js → chunk-KGLVRYIC-DW-FHk7T.js} +1 -1
  85. package/payload/server/public/assets/{chunk-LIHQZDEY-B9cFNN0l.js → chunk-LIHQZDEY-CGGVfAyG.js} +1 -1
  86. package/payload/server/public/assets/{chunk-ORNJ4GCN-BR3UNDTn.js → chunk-ORNJ4GCN-Bv8EUa5d.js} +1 -1
  87. package/payload/server/public/assets/{chunk-OYMX7WX6-2wKIEtFZ.js → chunk-OYMX7WX6-C1mXJnyw.js} +1 -1
  88. package/payload/server/public/assets/chunk-QZHKN3VN-CAwbQBtf.js +1 -0
  89. package/payload/server/public/assets/{chunk-U2HBQHQK-augM3AXB.js → chunk-U2HBQHQK-PwSQz6EP.js} +1 -1
  90. package/payload/server/public/assets/{chunk-X2U36JSP-BAM9q9ZQ.js → chunk-X2U36JSP--NPHTBlU.js} +1 -1
  91. package/payload/server/public/assets/{chunk-XPW4576I-DxPpji5P.js → chunk-XPW4576I-Bbc7Da32.js} +1 -1
  92. package/payload/server/public/assets/{chunk-YZCP3GAM-BD3ClFXP.js → chunk-YZCP3GAM-D-9v8e--.js} +1 -1
  93. package/payload/server/public/assets/{chunk-ZZ45TVLE-CqxhLPep.js → chunk-ZZ45TVLE-CMPg670Q.js} +1 -1
  94. package/payload/server/public/assets/classDiagram-6PBFFD2Q-Ct6Rn0vt.js +1 -0
  95. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-B_2XCIFv.js +1 -0
  96. package/payload/server/public/assets/clone-DjPWA_NH.js +1 -0
  97. package/payload/server/public/assets/{cose-bilkent-S5V4N54A-xywWX-9i.js → cose-bilkent-S5V4N54A-NVSb1Bhy.js} +1 -1
  98. package/payload/server/public/assets/{dagre-KV5264BT-ChRpqqiH.js → dagre-KV5264BT-V2Ai1DFl.js} +1 -1
  99. package/payload/server/public/assets/{dagre-BaHjDQ-8.js → dagre-QfLcO-1j.js} +1 -1
  100. package/payload/server/public/assets/data-BNWpKx1d.js +1 -0
  101. package/payload/server/public/assets/{diagram-5BDNPKRD-KjwTpyBi.js → diagram-5BDNPKRD-Caw21693.js} +1 -1
  102. package/payload/server/public/assets/{diagram-G4DWMVQ6-DZO9njAF.js → diagram-G4DWMVQ6-CgwVkT2q.js} +1 -1
  103. package/payload/server/public/assets/{diagram-MMDJMWI5-Bus-U-TI.js → diagram-MMDJMWI5-DFUrLZhs.js} +1 -1
  104. package/payload/server/public/assets/{diagram-TYMM5635-CfJodFgq.js → diagram-TYMM5635-CEEiEuZJ.js} +1 -1
  105. package/payload/server/public/assets/{erDiagram-SMLLAGMA-Bt5zqWiW.js → erDiagram-SMLLAGMA-CG10NrT3.js} +1 -1
  106. package/payload/server/public/assets/file-download-4mKJxUi-.js +1 -0
  107. package/payload/server/public/assets/{flatten-Bpu1L63d.js → flatten-DAO0sfCM.js} +1 -1
  108. package/payload/server/public/assets/{flowDiagram-DWJPFMVM-B64Q590S.js → flowDiagram-DWJPFMVM-BqkV9Okv.js} +1 -1
  109. package/payload/server/public/assets/{ganttDiagram-T4ZO3ILL-BWdgCEpH.js → ganttDiagram-T4ZO3ILL-DRiCDQF6.js} +1 -1
  110. package/payload/server/public/assets/gitGraph-7Q5UKJZL-B8Hx7f-D.js +1 -0
  111. package/payload/server/public/assets/{gitGraphDiagram-UUTBAWPF-Djq2B0WT.js → gitGraphDiagram-UUTBAWPF-BZV2voyy.js} +1 -1
  112. package/payload/server/public/assets/{graph-CrJ-itbq.js → graph-D0orIRAh.js} +3 -3
  113. package/payload/server/public/assets/{graph-labels-CmCWaKJT.js → graph-labels-B8YAdo3Z.js} +1 -1
  114. package/payload/server/public/assets/{graphlib-CNJh9_lx.js → graphlib-DGPThX8M.js} +1 -1
  115. package/payload/server/public/assets/info-OMHHGYJF-BicCJTqU.js +1 -0
  116. package/payload/server/public/assets/infoDiagram-42DDH7IO-tf-8rOez.js +2 -0
  117. package/payload/server/public/assets/{isEmpty-DkDEVnMZ.js → isEmpty-Cb_U81C6.js} +1 -1
  118. package/payload/server/public/assets/{ishikawaDiagram-UXIWVN3A-V3ACkWCW.js → ishikawaDiagram-UXIWVN3A-BKsAxSvp.js} +1 -1
  119. package/payload/server/public/assets/{journeyDiagram-VCZTEJTY-9T0qZeH3.js → journeyDiagram-VCZTEJTY-CmliuOd-.js} +1 -1
  120. package/payload/server/public/assets/{kanban-definition-6JOO6SKY-CxZ0ukx3.js → kanban-definition-6JOO6SKY-DAlElMb-.js} +1 -1
  121. package/payload/server/public/assets/{line-DUHKCQZT.js → line-DKZqLKAC.js} +1 -1
  122. package/payload/server/public/assets/{linear-BgIBblun.js → linear-CbSGdh1x.js} +1 -1
  123. package/payload/server/public/assets/{maximize-2-_cJWIgx2.js → maximize-2-CxstTgoK.js} +1 -1
  124. package/payload/server/public/assets/{mermaid-parser.core-CyHygs0-.js → mermaid-parser.core-DFvbao9C.js} +2 -2
  125. package/payload/server/public/assets/{mermaid.core-L55fJBSx.js → mermaid.core-C0vEBxDm.js} +3 -3
  126. package/payload/server/public/assets/{mindmap-definition-QFDTVHPH-DczaUX_E.js → mindmap-definition-QFDTVHPH-CShASUKd.js} +1 -1
  127. package/payload/server/public/assets/operator-DZzuotDB.js +1 -0
  128. package/payload/server/public/assets/{ordinal-DK3QISRR.js → ordinal-H0tVmBDP.js} +1 -1
  129. package/payload/server/public/assets/packet-4T2RLAQJ-Ckn_5hq0.js +1 -0
  130. package/payload/server/public/assets/{page-DbUaFfk6.js → page-3g0A19Rv.js} +6 -6
  131. package/payload/server/public/assets/page-Ck_pLhAk.js +1 -0
  132. package/payload/server/public/assets/{pdf-render-COLA4aPx.js → pdf-render-jAQU0nXt.js} +1 -1
  133. package/payload/server/public/assets/pie-ZZUOXDRM-DzlGQW3J.js +1 -0
  134. package/payload/server/public/assets/{pieDiagram-DEJITSTG-Da5TdnNt.js → pieDiagram-DEJITSTG-CBZHpDAJ.js} +1 -1
  135. package/payload/server/public/assets/{public-CYhDdDJd.js → public-WFUQw9AD.js} +1 -1
  136. package/payload/server/public/assets/{quadrantDiagram-34T5L4WZ-DX7J81uh.js → quadrantDiagram-34T5L4WZ-n3bB3K91.js} +1 -1
  137. package/payload/server/public/assets/radar-PYXPWWZC-BHo2YD8b.js +1 -0
  138. package/payload/server/public/assets/{reduce-uzWdoyL7.js → reduce-DAvwat-a.js} +1 -1
  139. package/payload/server/public/assets/{requirementDiagram-MS252O5E-DJQGrZPG.js → requirementDiagram-MS252O5E-DiDvQK3c.js} +1 -1
  140. package/payload/server/public/assets/rotate-ccw-COGQTi7n.js +1 -0
  141. package/payload/server/public/assets/{sankeyDiagram-XADWPNL6-Ba8fGz96.js → sankeyDiagram-XADWPNL6-DWDQQZmd.js} +1 -1
  142. package/payload/server/public/assets/{sequenceDiagram-FGHM5R23-BmDPC7qx.js → sequenceDiagram-FGHM5R23-DmAqRlBT.js} +1 -1
  143. package/payload/server/public/assets/{stateDiagram-FHFEXIEX-1VkptYFz.js → stateDiagram-FHFEXIEX-D4R-n6bZ.js} +1 -1
  144. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-BcFRbGRu.js +1 -0
  145. package/payload/server/public/assets/tasks-B97VpwEJ.js +1 -0
  146. package/payload/server/public/assets/{time-entry-format-D477WAes.js → time-entry-format-C09rDHo9.js} +1 -1
  147. package/payload/server/public/assets/{timeline-definition-GMOUNBTQ-B92U3T_f.js → timeline-definition-GMOUNBTQ-CjiHGP4l.js} +1 -1
  148. package/payload/server/public/assets/treeView-SZITEDCU-BtRr5vXp.js +1 -0
  149. package/payload/server/public/assets/treemap-W4RFUUIX-D8p0sbyo.js +1 -0
  150. package/payload/server/public/assets/useCopyFeedback-CSk9KjhY.js +1 -0
  151. package/payload/server/public/assets/useSubAccountSwitcher-DLJFISqf.js +9 -0
  152. package/payload/server/public/assets/{useSubAccountSwitcher-BKCWKo97.css → useSubAccountSwitcher-Dzmd7Aip.css} +1 -1
  153. package/payload/server/public/assets/{useVoiceRecorder-CZPtVpM-.js → useVoiceRecorder-DCyJQkFK.js} +2 -2
  154. package/payload/server/public/assets/{vennDiagram-DHZGUBPP-Y6Wkihrs.js → vennDiagram-DHZGUBPP-CBdDQ7c-.js} +1 -1
  155. package/payload/server/public/assets/wardley-RL74JXVD-DgMn-9ZJ.js +1 -0
  156. package/payload/server/public/assets/{wardleyDiagram-NUSXRM2D-iJpfal2z.js → wardleyDiagram-NUSXRM2D-BlIQPKLb.js} +1 -1
  157. package/payload/server/public/assets/{xychartDiagram-5P7HB3ND-S0bv7oH0.js → xychartDiagram-5P7HB3ND-CslKgh9B.js} +1 -1
  158. package/payload/server/public/browser.html +5 -5
  159. package/payload/server/public/calendar.html +6 -6
  160. package/payload/server/public/chat.html +13 -12
  161. package/payload/server/public/data.html +11 -11
  162. package/payload/server/public/graph.html +11 -10
  163. package/payload/server/public/index.html +14 -14
  164. package/payload/server/public/operator.html +15 -14
  165. package/payload/server/public/public.html +13 -12
  166. package/payload/server/public/tasks.html +5 -4
  167. package/payload/server/server.js +688 -539
  168. package/payload/server/public/assets/AdminShell-X9Bg3AKc.js +0 -2
  169. package/payload/server/public/assets/architecture-YZFGNWBL-Bbnxm4F4.js +0 -1
  170. package/payload/server/public/assets/browser-B0ieZy-b.js +0 -1
  171. package/payload/server/public/assets/calendar-CRL2_9Fl.js +0 -1
  172. package/payload/server/public/assets/channel-CatPTq-5.js +0 -1
  173. package/payload/server/public/assets/chat-DUc7bt5W.js +0 -1
  174. package/payload/server/public/assets/chevron-left-B2DKUxLk.js +0 -1
  175. package/payload/server/public/assets/chunk-426QAEUC-CqLMwOdA.js +0 -1
  176. package/payload/server/public/assets/chunk-QZHKN3VN-CjWy8NJ7.js +0 -1
  177. package/payload/server/public/assets/classDiagram-6PBFFD2Q-DHhR5rdN.js +0 -1
  178. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-DH4pAdfu.js +0 -1
  179. package/payload/server/public/assets/clone-DCKzwjWW.js +0 -1
  180. package/payload/server/public/assets/data-C1ftMulP.js +0 -1
  181. package/payload/server/public/assets/file-download-CHTyUQqS.js +0 -1
  182. package/payload/server/public/assets/gitGraph-7Q5UKJZL-Ps5HDzLh.js +0 -1
  183. package/payload/server/public/assets/info-OMHHGYJF-gtlpwIig.js +0 -1
  184. package/payload/server/public/assets/infoDiagram-42DDH7IO-CPrWlrsa.js +0 -2
  185. package/payload/server/public/assets/operator-B3Mi3TW0.js +0 -1
  186. package/payload/server/public/assets/packet-4T2RLAQJ-DrSn1e0n.js +0 -1
  187. package/payload/server/public/assets/page-C1G57kPZ.js +0 -1
  188. package/payload/server/public/assets/pie-ZZUOXDRM-C09i6N-g.js +0 -1
  189. package/payload/server/public/assets/radar-PYXPWWZC-CMZN2KQH.js +0 -1
  190. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-BdgkPHuh.js +0 -1
  191. package/payload/server/public/assets/tasks-0OcRiBdQ.js +0 -1
  192. package/payload/server/public/assets/treeView-SZITEDCU-CeuUM5oL.js +0 -1
  193. package/payload/server/public/assets/treemap-W4RFUUIX-C--fAaJv.js +0 -1
  194. package/payload/server/public/assets/useCopyFeedback-CpfVOO26.js +0 -1
  195. package/payload/server/public/assets/useSubAccountSwitcher-Bo7bnFtC.js +0 -9
  196. package/payload/server/public/assets/wardley-RL74JXVD-4WPJ2UA9.js +0 -1
  197. /package/payload/server/public/assets/{_baseFor-Q4DeIqQC.js → _baseFor-D3BqAOBr.js} +0 -0
  198. /package/payload/server/public/assets/{array-WaOTgZu7.js → array-iYP9fzJp.js} +0 -0
  199. /package/payload/server/public/assets/{cytoscape.esm-CMmjo4hO.js → cytoscape.esm-BQSBzPsB.js} +0 -0
  200. /package/payload/server/public/assets/{defaultLocale-BxqJVxq5.js → defaultLocale-BrdA1gDi.js} +0 -0
  201. /package/payload/server/public/assets/{dist-Dx-cpU53.js → dist-Cvgxm7qv.js} +0 -0
  202. /package/payload/server/public/assets/{init-DX7c5Edh.js → init-Bx0S6pkp.js} +0 -0
  203. /package/payload/server/public/assets/{katex-B2dtGfSp.js → katex-BsiH_a6d.js} +0 -0
  204. /package/payload/server/public/assets/{path-CnSyroar.js → path-BLSgTy3w.js} +0 -0
  205. /package/payload/server/public/assets/{preload-helper-DqboRNgS.js → preload-helper-ClBMtM1g.js} +0 -0
  206. /package/payload/server/public/assets/{rough.esm-eLccZ4OJ.js → rough.esm-BMOtDaaS.js} +0 -0
  207. /package/payload/server/public/assets/{src-Dqs1wlnb.js → src-DV4LKv2E.js} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-maxy-code",
3
- "version": "0.1.436",
3
+ "version": "0.1.437",
4
4
  "description": "Install Maxy — AI for Productive People",
5
5
  "bin": {
6
6
  "create-maxy-code": "./dist/index.js"
@@ -29,7 +29,21 @@ interface MinimalSession {
29
29
  }>;
30
30
  }>;
31
31
  }
32
+ export type ActiveRuleSource = "owner" | "no-owner" | "owner-error";
33
+ export type InjectionSource = ActiveRuleSource | "fetch-error";
34
+ export interface ActiveRuleResolution {
35
+ rules: ActiveRule[];
36
+ ownerUserId: string | null;
37
+ source: ActiveRuleSource;
38
+ }
39
+ export interface StandingRulesInjection {
40
+ block: string;
41
+ ownerUserId: string | null;
42
+ source: InjectionSource;
43
+ }
44
+ export declare function resolveOwnerUserId(session: MinimalSession, accountId: string): Promise<string | null>;
32
45
  export declare function queryActiveRuleCandidates(session: MinimalSession, accountId: string): Promise<RuleCandidate[]>;
46
+ export declare function resolveActiveRules(session: MinimalSession, accountId: string, cap?: number): Promise<ActiveRuleResolution>;
33
47
  export declare function activeRuleSet(session: MinimalSession, accountId: string, cap?: number): Promise<ActiveRule[]>;
34
48
  export {};
35
49
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,mBAAmB,OAAO,CAAC;AACxC,eAAO,MAAM,kBAAkB,KAAK,CAAC;AACrC,eAAO,MAAM,sBAAsB,MAAM,CAAC;AAC1C,eAAO,MAAM,sBAAsB,KAAK,CAAC;AAEzC,MAAM,WAAW,aAAa;IAC5B,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;CAC5B;AAED,MAAM,WAAW,UAAU;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,MAAM,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,MAAM,EAAE,GAAG,MAAM,CAYvD;AAOD,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,aAAa,EAAE,EACxB,eAAe,GAAE,MAA4B,GAC5C,UAAU,EAAE,CAgBd;AAED,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,UAAU,EAAE,EACnB,GAAG,GAAE,MAA2B,GAC/B,MAAM,CAIR;AAED,UAAU,cAAc;IACtB,GAAG,EAAE,CACH,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KACvB,OAAO,CAAC;QAAE,OAAO,EAAE,KAAK,CAAC;YAAE,GAAG,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;SAAE,CAAC,CAAA;KAAE,CAAC,CAAC;CAC/D;AAOD,wBAAsB,yBAAyB,CAC7C,OAAO,EAAE,cAAc,EACvB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,aAAa,EAAE,CAAC,CA6B1B;AAED,wBAAsB,aAAa,CACjC,OAAO,EAAE,cAAc,EACvB,SAAS,EAAE,MAAM,EACjB,GAAG,GAAE,MAA2B,GAC/B,OAAO,CAAC,UAAU,EAAE,CAAC,CAGvB"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,mBAAmB,OAAO,CAAC;AACxC,eAAO,MAAM,kBAAkB,KAAK,CAAC;AACrC,eAAO,MAAM,sBAAsB,MAAM,CAAC;AAC1C,eAAO,MAAM,sBAAsB,KAAK,CAAC;AAEzC,MAAM,WAAW,aAAa;IAC5B,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;CAC5B;AAED,MAAM,WAAW,UAAU;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,MAAM,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,MAAM,EAAE,GAAG,MAAM,CAYvD;AAOD,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,aAAa,EAAE,EACxB,eAAe,GAAE,MAA4B,GAC5C,UAAU,EAAE,CAgBd;AAED,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,UAAU,EAAE,EACnB,GAAG,GAAE,MAA2B,GAC/B,MAAM,CAIR;AAED,UAAU,cAAc;IACtB,GAAG,EAAE,CACH,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KACvB,OAAO,CAAC;QAAE,OAAO,EAAE,KAAK,CAAC;YAAE,GAAG,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;SAAE,CAAC,CAAA;KAAE,CAAC,CAAC;CAC/D;AAMD,MAAM,MAAM,gBAAgB,GAAG,OAAO,GAAG,UAAU,GAAG,aAAa,CAAC;AACpE,MAAM,MAAM,eAAe,GAAG,gBAAgB,GAAG,aAAa,CAAC;AAE/D,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,UAAU,EAAE,CAAC;IACpB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,MAAM,EAAE,gBAAgB,CAAC;CAC1B;AAID,MAAM,WAAW,sBAAsB;IACrC,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,MAAM,EAAE,eAAe,CAAC;CACzB;AAOD,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,cAAc,EACvB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAaxB;AAoED,wBAAsB,yBAAyB,CAC7C,OAAO,EAAE,cAAc,EACvB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,aAAa,EAAE,CAAC,CAE1B;AAGD,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,cAAc,EACvB,SAAS,EAAE,MAAM,EACjB,GAAG,GAAE,MAA2B,GAC/B,OAAO,CAAC,oBAAoB,CAAC,CAG/B;AAID,wBAAsB,aAAa,CACjC,OAAO,EAAE,cAAc,EACvB,SAAS,EAAE,MAAM,EACjB,GAAG,GAAE,MAA2B,GAC/B,OAAO,CAAC,UAAU,EAAE,CAAC,CAEvB"}
@@ -9,7 +9,9 @@ exports.ACTIVE_CANDIDATE_LIMIT = exports.ACTIVE_CANDIDATE_FLOOR = exports.INJECT
9
9
  exports.cosine = cosine;
10
10
  exports.deconflictBySubject = deconflictBySubject;
11
11
  exports.formatStandingRulesBlock = formatStandingRulesBlock;
12
+ exports.resolveOwnerUserId = resolveOwnerUserId;
12
13
  exports.queryActiveRuleCandidates = queryActiveRuleCandidates;
14
+ exports.resolveActiveRules = resolveActiveRules;
13
15
  exports.activeRuleSet = activeRuleSet;
14
16
  exports.SAME_SUBJECT_COSINE = 0.85;
15
17
  exports.INJECTION_RULE_CAP = 12;
@@ -55,19 +57,36 @@ function formatStandingRulesBlock(rules, cap = exports.INJECTION_RULE_CAP) {
55
57
  const lines = rules.slice(0, cap).map((r) => `- ${r.value}`);
56
58
  return `## Standing rules\n${lines.join("\n")}`;
57
59
  }
58
- // The candidate query: this account's admin-scope preferences, not trashed, not
59
- // a "does not apply" declination, above the injection floor, ordered so the
60
- // highest-confidence / most-recent wins de-confliction. Returns `embedding` for
61
- // the clustering step. The trashed predicate mirrors graph-trash's
60
+ // The account's owner identity (Task 1461). One `AdminUser {role:'owner'}` is
61
+ // seeded per account (Task 1359); `null` when absent (a pre-seeding legacy
62
+ // account). Mirrors the memory plugin's resolve-owner-userid.ts, but takes a
63
+ // session so this lib stays dependency-free (no neo4j-driver) — the same
64
+ // cross-process lockstep as the duplicated literals elsewhere in this tree.
65
+ async function resolveOwnerUserId(session, accountId) {
66
+ const res = await session.run(`
67
+ MATCH (au:AdminUser {accountId: $accountId, role: 'owner'})
68
+ RETURN au.userId AS userId
69
+ ORDER BY au.createdAt ASC
70
+ LIMIT 1
71
+ `, { accountId });
72
+ const rec = res.records[0];
73
+ const userId = rec ? rec.get("userId") : null;
74
+ return typeof userId === "string" && userId.length > 0 ? userId : null;
75
+ }
76
+ // The owner's admin-scope candidates, selected through the owner's
77
+ // `UserProfile` and pinned to the resolved owner userId so the prefs selected
78
+ // and the owner logged are one identity. Not trashed, not a "does not apply"
79
+ // declination, above the injection floor, ordered so the highest-confidence /
80
+ // most-recent wins de-confliction. The trashed predicate mirrors graph-trash's
62
81
  // `notTrashed()` exactly (label + deletedAt) so this lib stays dependency-free.
63
- async function queryActiveRuleCandidates(session, accountId) {
82
+ async function queryOwnerCandidates(session, accountId, ownerUserId) {
64
83
  // ACTIVE_CANDIDATE_LIMIT is inlined as a Cypher literal rather than bound as a
65
84
  // param: a plain JS number bound to `LIMIT $p` serializes as a FLOAT and Neo4j
66
85
  // rejects it ("not a valid value. Must be a non-negative integer"). This lib
67
86
  // is dependency-free (no neo4j-driver), so it cannot call `neo4j.int()`; the
68
87
  // constant is a hardcoded integer, so inlining it is safe and gate-clean.
69
88
  const result = await session.run(`
70
- MATCH (pref:Preference {accountId: $accountId, scope: 'admin'})
89
+ MATCH (up:UserProfile {accountId: $accountId, userId: $ownerUserId})-[:HAS_PREFERENCE]->(pref:Preference {scope: 'admin'})
71
90
  WHERE NOT pref:Trashed AND pref.deletedAt IS NULL
72
91
  AND coalesce(pref.notApplicable, false) = false
73
92
  AND pref.confidence >= $floor
@@ -76,7 +95,7 @@ async function queryActiveRuleCandidates(session, accountId) {
76
95
  pref.observedAt AS observedAt, pref.embedding AS embedding
77
96
  ORDER BY pref.confidence DESC, pref.observedAt DESC
78
97
  LIMIT ${exports.ACTIVE_CANDIDATE_LIMIT}
79
- `, { accountId, floor: exports.ACTIVE_CANDIDATE_FLOOR });
98
+ `, { accountId, ownerUserId, floor: exports.ACTIVE_CANDIDATE_FLOOR });
80
99
  return result.records.map((r) => ({
81
100
  preferenceId: r.get("preferenceId"),
82
101
  category: r.get("category"),
@@ -87,8 +106,36 @@ async function queryActiveRuleCandidates(session, accountId) {
87
106
  embedding: r.get("embedding") ?? null,
88
107
  }));
89
108
  }
109
+ // Resolve the owner then select. Injection has exactly one selection rule — the
110
+ // owner's prefs. No owner (or an owner-resolution error) yields no candidates
111
+ // rather than falling back to a different selection key. An owner-resolution
112
+ // error is caught here and degrades to empty; it never throws.
113
+ async function resolveCandidates(session, accountId) {
114
+ let ownerUserId;
115
+ try {
116
+ ownerUserId = await resolveOwnerUserId(session, accountId);
117
+ }
118
+ catch {
119
+ return { candidates: [], ownerUserId: null, source: "owner-error" };
120
+ }
121
+ if (!ownerUserId)
122
+ return { candidates: [], ownerUserId: null, source: "no-owner" };
123
+ const candidates = await queryOwnerCandidates(session, accountId, ownerUserId);
124
+ return { candidates, ownerUserId, source: "owner" };
125
+ }
126
+ // Owner-scoped candidates for a downstream reader that only needs the rows
127
+ // (the standing preference-reconciliation audit). Signature unchanged.
128
+ async function queryActiveRuleCandidates(session, accountId) {
129
+ return (await resolveCandidates(session, accountId)).candidates;
130
+ }
131
+ // The account's active, de-conflicted, bounded rule-set plus its provenance.
132
+ async function resolveActiveRules(session, accountId, cap = exports.INJECTION_RULE_CAP) {
133
+ const { candidates, ownerUserId, source } = await resolveCandidates(session, accountId);
134
+ return { rules: deconflictBySubject(candidates).slice(0, cap), ownerUserId, source };
135
+ }
136
+ // The rule array alone — the unchanged call boundary for profile-read and the
137
+ // ui/server injection wrapper.
90
138
  async function activeRuleSet(session, accountId, cap = exports.INJECTION_RULE_CAP) {
91
- const candidates = await queryActiveRuleCandidates(session, accountId);
92
- return deconflictBySubject(candidates).slice(0, cap);
139
+ return (await resolveActiveRules(session, accountId, cap)).rules;
93
140
  }
94
141
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -3,6 +3,9 @@ import {
3
3
  cosine,
4
4
  deconflictBySubject,
5
5
  formatStandingRulesBlock,
6
+ resolveActiveRules,
7
+ activeRuleSet,
8
+ queryActiveRuleCandidates,
6
9
  INJECTION_RULE_CAP,
7
10
  type RuleCandidate,
8
11
  } from "./index.js";
@@ -68,6 +71,109 @@ describe("deconflictBySubject", () => {
68
71
  });
69
72
  });
70
73
 
74
+ // A fake Neo4j session that dispatches on query text: the owner-resolution
75
+ // query (matches `role: 'owner'`) returns a configurable owner userId or none;
76
+ // the candidate query (matches HAS_PREFERENCE) returns configurable pref rows
77
+ // and asserts it was pinned to the resolved owner userId.
78
+ function fakeSession(opts: {
79
+ ownerId?: string | null;
80
+ ownerThrows?: boolean;
81
+ prefRows?: Array<Record<string, unknown>>;
82
+ }) {
83
+ return {
84
+ calls: [] as string[],
85
+ candidateRuns: 0,
86
+ async run(q: string, p: Record<string, unknown>) {
87
+ this.calls.push(q);
88
+ if (/role:\s*'owner'/.test(q)) {
89
+ if (opts.ownerThrows) throw new Error("neo4j down");
90
+ const recs = opts.ownerId
91
+ ? [{ get: (_k: string) => opts.ownerId }]
92
+ : [];
93
+ return { records: recs };
94
+ }
95
+ // candidate query — must be pinned to the resolved owner userId
96
+ this.candidateRuns++;
97
+ expect(p.ownerUserId).toBe(opts.ownerId);
98
+ return {
99
+ records: (opts.prefRows ?? []).map((r) => ({
100
+ get: (k: string) => r[k],
101
+ })),
102
+ };
103
+ },
104
+ };
105
+ }
106
+
107
+ const prefRow = (over: Partial<RuleCandidate>): Record<string, unknown> => ({
108
+ preferenceId: "p",
109
+ category: "communication",
110
+ key: "k",
111
+ value: "v",
112
+ confidence: 0.6,
113
+ observedAt: "2026-07-01T00:00:00Z",
114
+ embedding: null,
115
+ ...over,
116
+ });
117
+
118
+ describe("resolveActiveRules — owner-scoped selection", () => {
119
+ it("returns the owner's prefs with source=owner when an owner exists", async () => {
120
+ const s = fakeSession({
121
+ ownerId: "owner-35fed05e",
122
+ prefRows: [
123
+ prefRow({ preferenceId: "a", value: "brand", confidence: 0.9 }),
124
+ prefRow({ preferenceId: "b", value: "pricing", confidence: 0.7 }),
125
+ prefRow({ preferenceId: "c", value: "tender", confidence: 0.5 }),
126
+ ],
127
+ });
128
+ const res = await resolveActiveRules(s, "acct-1");
129
+ expect(res.source).toBe("owner");
130
+ expect(res.ownerUserId).toBe("owner-35fed05e");
131
+ expect(res.rules.map((r) => r.preferenceId)).toEqual(["a", "b", "c"]);
132
+ expect(s.candidateRuns).toBe(1);
133
+ });
134
+
135
+ it("returns empty with source=no-owner and never runs the candidate query", async () => {
136
+ const s = fakeSession({ ownerId: null });
137
+ const res = await resolveActiveRules(s, "acct-1");
138
+ expect(res.source).toBe("no-owner");
139
+ expect(res.ownerUserId).toBeNull();
140
+ expect(res.rules).toEqual([]);
141
+ expect(s.candidateRuns).toBe(0);
142
+ });
143
+
144
+ it("returns empty with source=owner when the owner has zero prefs", async () => {
145
+ const s = fakeSession({ ownerId: "owner-x", prefRows: [] });
146
+ const res = await resolveActiveRules(s, "acct-1");
147
+ expect(res.source).toBe("owner");
148
+ expect(res.ownerUserId).toBe("owner-x");
149
+ expect(res.rules).toEqual([]);
150
+ expect(s.candidateRuns).toBe(1);
151
+ });
152
+
153
+ it("degrades to empty with source=owner-error on an owner-resolution failure (never throws)", async () => {
154
+ const s = fakeSession({ ownerThrows: true });
155
+ const res = await resolveActiveRules(s, "acct-1");
156
+ expect(res.source).toBe("owner-error");
157
+ expect(res.ownerUserId).toBeNull();
158
+ expect(res.rules).toEqual([]);
159
+ expect(s.candidateRuns).toBe(0);
160
+ });
161
+
162
+ it("activeRuleSet returns just the owner's rule array", async () => {
163
+ const s = fakeSession({
164
+ ownerId: "owner-1",
165
+ prefRows: [prefRow({ preferenceId: "a" })],
166
+ });
167
+ const rules = await activeRuleSet(s, "acct-1");
168
+ expect(rules.map((r) => r.preferenceId)).toEqual(["a"]);
169
+ });
170
+
171
+ it("queryActiveRuleCandidates stays owner-scoped (empty on no owner)", async () => {
172
+ const s = fakeSession({ ownerId: null });
173
+ expect(await queryActiveRuleCandidates(s, "acct-1")).toEqual([]);
174
+ });
175
+ });
176
+
71
177
  describe("formatStandingRulesBlock", () => {
72
178
  it("returns empty string for no rules", () => {
73
179
  expect(formatStandingRulesBlock([], INJECTION_RULE_CAP)).toBe("");
@@ -84,14 +84,60 @@ interface MinimalSession {
84
84
  ) => Promise<{ records: Array<{ get(k: string): unknown }> }>;
85
85
  }
86
86
 
87
- // The candidate query: this account's admin-scope preferences, not trashed, not
88
- // a "does not apply" declination, above the injection floor, ordered so the
89
- // highest-confidence / most-recent wins de-confliction. Returns `embedding` for
90
- // the clustering step. The trashed predicate mirrors graph-trash's
87
+ // Task 1564 which admin `AdminUser`/`UserProfile` owns a preference now
88
+ // decides whether it is injected. `source` records how the rule-set was
89
+ // selected; `InjectionSource` widens it with `fetch-error`, produced only by
90
+ // the ui/server never-throw catch (a hard Neo4j failure), never by this lib.
91
+ export type ActiveRuleSource = "owner" | "no-owner" | "owner-error";
92
+ export type InjectionSource = ActiveRuleSource | "fetch-error";
93
+
94
+ export interface ActiveRuleResolution {
95
+ rules: ActiveRule[];
96
+ ownerUserId: string | null;
97
+ source: ActiveRuleSource;
98
+ }
99
+
100
+ // The per-turn injection payload the UI hands each channel gateway: the
101
+ // formatted `## Standing rules` block plus the provenance the gateway logs.
102
+ export interface StandingRulesInjection {
103
+ block: string;
104
+ ownerUserId: string | null;
105
+ source: InjectionSource;
106
+ }
107
+
108
+ // The account's owner identity (Task 1461). One `AdminUser {role:'owner'}` is
109
+ // seeded per account (Task 1359); `null` when absent (a pre-seeding legacy
110
+ // account). Mirrors the memory plugin's resolve-owner-userid.ts, but takes a
111
+ // session so this lib stays dependency-free (no neo4j-driver) — the same
112
+ // cross-process lockstep as the duplicated literals elsewhere in this tree.
113
+ export async function resolveOwnerUserId(
114
+ session: MinimalSession,
115
+ accountId: string,
116
+ ): Promise<string | null> {
117
+ const res = await session.run(
118
+ `
119
+ MATCH (au:AdminUser {accountId: $accountId, role: 'owner'})
120
+ RETURN au.userId AS userId
121
+ ORDER BY au.createdAt ASC
122
+ LIMIT 1
123
+ `,
124
+ { accountId },
125
+ );
126
+ const rec = res.records[0];
127
+ const userId = rec ? rec.get("userId") : null;
128
+ return typeof userId === "string" && userId.length > 0 ? userId : null;
129
+ }
130
+
131
+ // The owner's admin-scope candidates, selected through the owner's
132
+ // `UserProfile` and pinned to the resolved owner userId so the prefs selected
133
+ // and the owner logged are one identity. Not trashed, not a "does not apply"
134
+ // declination, above the injection floor, ordered so the highest-confidence /
135
+ // most-recent wins de-confliction. The trashed predicate mirrors graph-trash's
91
136
  // `notTrashed()` exactly (label + deletedAt) so this lib stays dependency-free.
92
- export async function queryActiveRuleCandidates(
137
+ async function queryOwnerCandidates(
93
138
  session: MinimalSession,
94
139
  accountId: string,
140
+ ownerUserId: string,
95
141
  ): Promise<RuleCandidate[]> {
96
142
  // ACTIVE_CANDIDATE_LIMIT is inlined as a Cypher literal rather than bound as a
97
143
  // param: a plain JS number bound to `LIMIT $p` serializes as a FLOAT and Neo4j
@@ -100,7 +146,7 @@ export async function queryActiveRuleCandidates(
100
146
  // constant is a hardcoded integer, so inlining it is safe and gate-clean.
101
147
  const result = await session.run(
102
148
  `
103
- MATCH (pref:Preference {accountId: $accountId, scope: 'admin'})
149
+ MATCH (up:UserProfile {accountId: $accountId, userId: $ownerUserId})-[:HAS_PREFERENCE]->(pref:Preference {scope: 'admin'})
104
150
  WHERE NOT pref:Trashed AND pref.deletedAt IS NULL
105
151
  AND coalesce(pref.notApplicable, false) = false
106
152
  AND pref.confidence >= $floor
@@ -110,7 +156,7 @@ export async function queryActiveRuleCandidates(
110
156
  ORDER BY pref.confidence DESC, pref.observedAt DESC
111
157
  LIMIT ${ACTIVE_CANDIDATE_LIMIT}
112
158
  `,
113
- { accountId, floor: ACTIVE_CANDIDATE_FLOOR },
159
+ { accountId, ownerUserId, floor: ACTIVE_CANDIDATE_FLOOR },
114
160
  );
115
161
  return result.records.map((r) => ({
116
162
  preferenceId: r.get("preferenceId") as string,
@@ -123,11 +169,54 @@ export async function queryActiveRuleCandidates(
123
169
  }));
124
170
  }
125
171
 
172
+ // Resolve the owner then select. Injection has exactly one selection rule — the
173
+ // owner's prefs. No owner (or an owner-resolution error) yields no candidates
174
+ // rather than falling back to a different selection key. An owner-resolution
175
+ // error is caught here and degrades to empty; it never throws.
176
+ async function resolveCandidates(
177
+ session: MinimalSession,
178
+ accountId: string,
179
+ ): Promise<{
180
+ candidates: RuleCandidate[];
181
+ ownerUserId: string | null;
182
+ source: ActiveRuleSource;
183
+ }> {
184
+ let ownerUserId: string | null;
185
+ try {
186
+ ownerUserId = await resolveOwnerUserId(session, accountId);
187
+ } catch {
188
+ return { candidates: [], ownerUserId: null, source: "owner-error" };
189
+ }
190
+ if (!ownerUserId) return { candidates: [], ownerUserId: null, source: "no-owner" };
191
+ const candidates = await queryOwnerCandidates(session, accountId, ownerUserId);
192
+ return { candidates, ownerUserId, source: "owner" };
193
+ }
194
+
195
+ // Owner-scoped candidates for a downstream reader that only needs the rows
196
+ // (the standing preference-reconciliation audit). Signature unchanged.
197
+ export async function queryActiveRuleCandidates(
198
+ session: MinimalSession,
199
+ accountId: string,
200
+ ): Promise<RuleCandidate[]> {
201
+ return (await resolveCandidates(session, accountId)).candidates;
202
+ }
203
+
204
+ // The account's active, de-conflicted, bounded rule-set plus its provenance.
205
+ export async function resolveActiveRules(
206
+ session: MinimalSession,
207
+ accountId: string,
208
+ cap: number = INJECTION_RULE_CAP,
209
+ ): Promise<ActiveRuleResolution> {
210
+ const { candidates, ownerUserId, source } = await resolveCandidates(session, accountId);
211
+ return { rules: deconflictBySubject(candidates).slice(0, cap), ownerUserId, source };
212
+ }
213
+
214
+ // The rule array alone — the unchanged call boundary for profile-read and the
215
+ // ui/server injection wrapper.
126
216
  export async function activeRuleSet(
127
217
  session: MinimalSession,
128
218
  accountId: string,
129
219
  cap: number = INJECTION_RULE_CAP,
130
220
  ): Promise<ActiveRule[]> {
131
- const candidates = await queryActiveRuleCandidates(session, accountId);
132
- return deconflictBySubject(candidates).slice(0, cap);
221
+ return (await resolveActiveRules(session, accountId, cap)).rules;
133
222
  }
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: platform-architecture
3
3
  description: Use when grounding any documented-surface claim about what Maxy ships — plugins, skills, specialists, install/deploy flows, internals. This is the install catalogue, not evidence of what is enabled on the current account. For install state on this account, call `capabilities-here`; for documented surface, cite the `Source:` URL inline.
4
- content-hash: sha256:bd08bc341313562b1dd2139010b1120b23d7aab130e1d32c3e800678ad559f7c
4
+ content-hash: sha256:359dfc8bdd471d36f84a6cf223c8a1621dd66f00245f5e8f8e2674642036601b
5
5
  brand: maxy-code
6
6
  product-name: Maxy
7
7
  ---
@@ -2737,7 +2737,7 @@ either is a regression.
2737
2737
  |---|---|---|
2738
2738
  | `/session` | Admin cookie session: PIN-gated mint, validate, rotate. | `GET /`, `POST /` |
2739
2739
  | `/sessions` | Legacy admin-server conversation routes. No UI consumer remains after the ConversationsModal was retired; the surviving handlers are deletion candidates and not described here. | (legacy, no live caller) |
2740
- | `/sidebar-sessions` | Sole data path for the sidebar Sessions list. One JSONL on disk equals one row. The row's delete button is the only way a row disappears. Each row carries `sessionId`, `title`, `startedAt`, `live`, `isSubagent`, `pid: number \| null` (basename of the matched `sessions/<pid>.json`), and `projectDir` (the directory holding the JSONL — consumed by the delete route). The payload also carries top-level `accountId` so the pane renders the full UUID label whose first ~8 chars prefix-match the truncated Remote Control daemon entry in claude.ai/code. The legacy `rcUrl` field is gone — the row's external-link affordance now POSTs `/session-rc-spawn` to start a fresh local `claude --remote-control <name> --session-id <sid>` PTY on every click. | `GET /` |
2740
+ | `/sidebar-sessions` | Sole data path for the sidebar Sessions list. One JSONL on disk equals one row. The row's delete button is the only way a row disappears. Each row carries `sessionId`, `title`, `startedAt`, `live`, `isSubagent`, `pid: number \| null` (basename of the matched `sessions/<pid>.json`), and `projectDir` (the directory holding the JSONL — consumed by the delete route). The payload also carries top-level `accountId` so the pane renders the full UUID label whose first ~8 chars prefix-match the truncated Remote Control daemon entry in claude.ai/code. The legacy `rcUrl` field is gone — the row's external-link affordance now POSTs `/session-rc-spawn` to start a fresh local `claude --remote-control <name> --session-id <sid>` PTY on every click. **WhatsApp and Telegram sessions (admin + public) appear in this list**, colour-coded by channel (WhatsApp green `#25D366`, Telegram blue `#229ED9`, via a `conv-channel-*` left accent bar) and carrying the standard row kebab (Usage & cost, Reset/re-seat, Rename, Archive, Delete) so the operator can inspect and re-seat a channel session's params. Each row's `channel` is the authoritative `.meta.json` sidecar value (`whatsapp`/`telegram`/`webchat`/`browser`/null), not the JSONL-envelope channel. The only role/channel excluded from the list is `public`+`webchat` (its dedicated reader is the sole surface); the exclusion predicate is `isSessionListExcluded`, decoupled from the reader's inclusion predicate `isReaderChannelSession` (unchanged), so the WhatsApp/Telegram reader panels are unaffected — a store-only WhatsApp conversation with no session JSONL stays reader-only. Observability rides `[admin-sessions-list] … channelInList=whatsapp:<n>,telegram:<n>,webchat:<n> unknownChannel=<n> excludedPublicWebchat=<n>`. | `GET /` |
2741
2741
  | `/session-delete` | POST `{ sessionId }`. Thin proxy over the manager: POST `/:id/stop` (idempotent) then DELETE `/:id`. Sends **no** accountId. The manager resolves the session by scanning every slug under `projects/` for its JSONL (`slugForExistingJsonl`, covering `archive/`), exactly as the sidebar list enumerates rows, and gates on file existence alone — so any session the operator can see is deletable. This replaces the earlier accountId-derived slug and the `!row` term, which narrowed resolution below the list and left a session under a non-house slug with no watcher row (e.g. a legacy `-home-admin` session on a single-account install) listed yet un-deletable. Account scope is a view filter, not a delete gate — admin access is install-wide, so there is nothing to scope against. The sibling `/session-stop` (the standalone End control) resolves identically and also forwards no accountId. | `POST /` |
2742
2742
  | `/session-rc-spawn` | POST `{ sessionId?, name? }`. Fire-and-forget `claude --remote-control [name] [--session-id <sid>]`. Present `sessionId` resumes; absent starts a fresh session (also used by the sidebar's "New session" button — it no longer opens claude.ai/code directly). Proxies to the manager's `/rc-spawn`, which waits up to **60 s** (raised from 12 s) for the spawned PTY to bind and returns `{ spawnedPid, sessionId, bridgeSessionId, slug, outcome, reason }`. For a webchat-bound spawn (every admin-gated host's "New session", returning a same-origin `/chat?session=<id>` target — `resolveRcSpawnOutcome` → `sameOrigin:true`) the Sidebar navigates the **current** tab via `window.location.assign`, replacing the dashboard in place (back returns to it); only a claude.ai/code slug (`sameOrigin:false`, the bare-admin resume bridge, never a new-session outcome) navigates a separately-opened tab. On `timeout` or `spawn-failed` it shows an error modal (reason + sessionId) and **never** opens a bare claude.ai/code tab. The new process registers itself as its own Remote Control entry in claude.ai/code. | `POST /` |
2743
2743
  | `/claude-sessions` | **Spawn surface only**. `POST /` is the Sidebar new-session-with-prompt path, cookie-auth only (the recorder loopback caller was removed; LinkedIn ingest moved to `/rc-spawn`). The former UI-facing handlers (SSE row feed, list, resume, stop, rename, archive, delete, `/:id/meta`, `/:id/input`, `/:id/log`) were removed — the maxy dashboard no longer manages or displays sessions. | `POST /` |
@@ -2750,7 +2750,7 @@ either is a regression.
2750
2750
 
2751
2751
  **`/chat` composer transport button.** The button at the end of the input shows a **microphone** while the box is empty — tap it to record a voice note. Type a character, or attach a file, and the same button becomes the **send** arrow; while the agent is replying it becomes a **stop** square. So an empty composer offers voice, a composer with something to send offers send, and a running turn offers stop — one button, three states. The microphone-device chooser (which input to record from) stays in the row of icons below the box. This is the same on the admin `/chat`, the public chat, and the maxy-lite webchat.
2752
2752
 
2753
- **`/chat` Conversations flyout + zero-sessions splash.** The admin `/chat` `HeaderMenu` carries a **Conversations** item (rendered only when `onOpenConversations` is wired, which scopes it to `/chat`) that opens an in-chat session-management pane (`app/chat/SessionList.tsx`) hosted by `chat/page.tsx` — distinct from, and sharing endpoints with, the `Sidebar` Sessions list (which stays hidden on narrow viewports). It enumerates the admin's own webchat sessions via `GET /api/admin/sidebar-sessions` (carries the per-row `live` marker and `archived` flag, install-wide), lists them newest-first with the live one marked and archived rows folded under a collapsible subsection, and offers per-row resume (`/chat?session=<id>`), rename, archive/unarchive, two-tap delete, an **End** control on the live row (`session-stop`), plus a New-session control (`session-rc-spawn`) and a copyable full id. The pane header carries the title and a top-right close (X) control that dismisses the flyout (no back-chevron/"Chat" label); the New-session control floats at the pane's bottom-left. When the canonical pointer is `known:false` (`canonical-empty`) and enumeration returns zero rows, the surface renders a splash (brand logo + New session) instead of a dead bootstrap thread; a freshly-spawned New session is `known:true`, so its greeting is preserved. Client breadcrumbs: `[chat-conversations] op=enumerate owned=<n>` and `op=action name=<open|rename|archive|delete|stop> sessionId=<id8>` (`op=action-failed … status=…` on a non-2xx). Detail lives in `.docs/admin-webchat-native-channel.md` ("Conversations flyout + zero-sessions splash").
2753
+ **`/chat` Conversations flyout + zero-sessions splash.** The admin `/chat` `HeaderMenu` carries a **Conversations** item (rendered only when `onOpenConversations` is wired, which scopes it to `/chat`) that opens an in-chat session-management pane (`app/chat/SessionList.tsx`) hosted by `chat/page.tsx` — distinct from, and sharing endpoints with, the `Sidebar` Sessions list (which stays hidden on narrow viewports). It enumerates the admin's own webchat sessions via `GET /api/admin/sidebar-sessions` (carries the per-row `live` marker and `archived` flag, install-wide), **filtering out `channel:'whatsapp'`/`'telegram'` rows** (those are channel-bound, cannot be chatted in webchat, and live in the WhatsApp/Telegram reader panels; the same endpoint now also feeds the sidebar Sessions list, which does show them), lists the remainder newest-first with the live one marked and archived rows folded under a collapsible subsection, and offers per-row resume (`/chat?session=<id>`), rename, archive/unarchive, two-tap delete, an **End** control on the live row (`session-stop`), plus a New-session control (`session-rc-spawn`) and a copyable full id. The pane header carries the title and a top-right close (X) control that dismisses the flyout (no back-chevron/"Chat" label); the New-session control floats at the pane's bottom-left. When the canonical pointer is `known:false` (`canonical-empty`) and enumeration returns zero rows, the surface renders a splash (brand logo + New session) instead of a dead bootstrap thread; a freshly-spawned New session is `known:true`, so its greeting is preserved. Client breadcrumbs: `[chat-conversations] op=enumerate owned=<n>` and `op=action name=<open|rename|archive|delete|stop> sessionId=<id8>` (`op=action-failed … status=…` on a non-2xx). Detail lives in `.docs/admin-webchat-native-channel.md` ("Conversations flyout + zero-sessions splash").
2754
2754
 
2755
2755
  **`/chat` header always shows the account name.** The admin/operator `HeaderMenu` centre always renders the active account's name alongside the brand logo, on every chat surface, regardless of the active conversation — `headerTitle = businessName || brand.productName`. There is no per-conversation header title: the active conversation's label stays in the sidebar and the Conversations flyout (each resolves its own per-session title independently), never in the header. The account name is `LocalBusiness.name`, resolved by `fetchAccountName(accountId)` in `neo4j-store.ts` with **no theme gate** — unlike `fetchBranding` (which returns null unless a colour/logo property is also set, and stays the source for actual theming). The house account is seeded with `LocalBusiness.name = <brand productName>` (`seed-neo4j.sh`, coalesced ON MATCH so a re-seed backfills a name-less node and never clobbers a name the business-profile recorder set later), so the sub-account picker's house row shows the product name (e.g. "SiteDesk"), not the raw accountId UUID. When a name is absent the resolver emits `[branding] account-name-fallback accountId=<id8>… reason=no-name`, and the picker falls back to the accountId; the seed + backfill is the name source. The lite variant header is unchanged.
2756
2756
 
@@ -3871,11 +3871,11 @@ Standing rules the operator gives over a channel are `Preference` nodes. Two mec
3871
3871
 
3872
3872
  **Reconcile by meaning on write (`profile-update`).** The existence check is two-tier. First an exact `(category, key)` match — the classic fast path, unchanged. When that misses, a semantic-neighbour lookup runs over the `preference_embedding` vector index, scoped to the same `accountId` and `scope:'admin'`, non-trashed. If the nearest neighbour is at or above `SAME_SUBJECT_COSINE` (0.85), the write lands on that node through the existing reinforce/update/contradict curve instead of minting a duplicate; the matched neighbours are returned in the tool result so the agent can classify future writes against real prior state. The confidence constants are unchanged — only the join key widens, so a rephrasing under a new `key` no longer forks the rule and a correction demotes the prior node rather than adding a contradictory one.
3873
3873
 
3874
- **Inject the active rule-set every turn (channel gateways).** The one definition of "this account's active rules" is `@maxy/active-rules`: a bounded, floor-filtered (`confidence >= 0.4`), confidence-ordered candidate query, then semantic de-confliction (one winner per subject at the same 0.85 cosine), capped to `INJECTION_RULE_CAP` (12). `profile-read` exposes this set as `activeRules`, and the three inbound gateways (`wa-gateway`, `telegram-gateway`, `webchat-gateway`) fetch it for the effective session account and attach it as `standingRules` on the inbound frame; the channel builders prepend it as a `## Standing rules` block above `## Context`. Both channel-server inbound paths carry the block: the single-target SSE loop parses the whole frame, and the unified multi-target door reader (`runDoorStream`,) reconstructs it through the shared `doorInboundPayload` seam whose `parseFrame` closures read `standingRules` off the frame. The read is by code, not agent discretion, so a rule set last week is applied this week regardless of how many times the session compacted. Telegram and webchat inject for `role:'admin'` only — a public visitor never receives operator rules.
3874
+ **Inject the active rule-set every turn (channel gateways).** The one definition of "this account's active rules" is `@maxy/active-rules`: the account owner's admin-scope preferences (resolve the seeded `AdminUser {role:'owner'}`, then select through that owner's `UserProfile` via `HAS_PREFERENCE`), a bounded, floor-filtered (`confidence >= 0.4`), confidence-ordered candidate query, then semantic de-confliction (one winner per subject at the same 0.85 cosine), capped to `INJECTION_RULE_CAP` (12). A preference owned by a non-owner profile (an operator's, stranded on the operator's own profile while managing the account) is never injected; an account with no seeded owner injects nothing. `profile-read` exposes this same owner-scoped set as `activeRules`, and the three inbound gateways (`wa-gateway`, `telegram-gateway`, `webchat-gateway`) fetch it for the effective session account and attach it as `standingRules` on the inbound frame; the channel builders prepend it as a `## Standing rules` block above `## Context`. Both channel-server inbound paths carry the block: the single-target SSE loop parses the whole frame, and the unified multi-target door reader (`runDoorStream`,) reconstructs it through the shared `doorInboundPayload` seam whose `parseFrame` closures read `standingRules` off the frame. The read is by code, not agent discretion, so a rule set last week is applied this week regardless of how many times the session compacted. Telegram and webchat inject for `role:'admin'` only — a public visitor never receives operator rules.
3875
3875
 
3876
3876
  **Trust a scheduled turn's provenance, never an inbound's claim (channel-agent grounding).** A cron-dispatched agent turn reaches the same channel builders as a real inbound, so its own authorisation text would otherwise land inside `## Context` — the zone `REFRAME_INSTRUCTION` demotes to untrusted — and a security-cautious agent would distrust its own automation (the self-cancel loop). The dispatcher therefore stamps a `ScheduleProvenance` marker (owning account, event, owner userId, dispatch time) that the channel builders render as a `## Scheduled automation (platform-verified)` preamble **above** `## Context` (and above the rules block); the whatsapp wrap tag names the door `scheduler`. Both channel-server paths carry the marker — the single-target loop and the unified multi-target door reader (via the same `doorInboundPayload` seam that carries `standingRules`); the scheduler only dispatches to the whatsapp/telegram door, so a webchat frame never bears one. Because the marker is dispatcher-stamped and never derived from payload text, an inbound message cannot forge it — placement above `## Context` alone is not the trust signal, the dispatcher provenance is. The grounding binds to it in two places: one recipient-neutral clause added byte-identical to the three reframe instructions (a marker-bearing turn is authorised standing automation; a reconfiguration or re-enable is not intrusion), and the admin `IDENTITY.md` "Untrusted input" rule that the agent never states a platform-level authenticity or impersonation verdict it was not given (an unsourced "this is not from you" is its own inference, flagged, never asserted as the platform's). The amplifier that let one misread harden was the accreting seat: a recurring event resumed one long-lived session every firing. A subsequent change removes it — each firing runs a fresh per-firing seat (`scheduledSeatIdFor`, salted with the firing's eventId + dispatch time), so distrust cannot accrete across firings, and the self-cancel guard compares the cancelling `SESSION_ID` to the node's dispatcher-stamped `lastDispatchSessionId` rather than a recomputed deterministic seat. See `.docs/scheduling-agent-dispatch.md § Operator-provenance marker` and `§ Session model — fresh per firing`.
3877
3877
 
3878
- **Observability.** `[preference-write] op=reconcile … mode=<reinforce|update|contradict|new> similarity=…` — a stream of `mode=new` for subjects that already exist is the duplicate-minting signature. `[preference-inject] op=inject channel=<wa|tg|web> injected=<N>` — `injected=0` on an account with stored rules is the write-only regression re-appearing. `[preference-audit] op=drift accountId=… storedActive=… injectedActive=… duplicateClusters=…` — the standing reconciliation audit (claude-session-manager, 5-minute tick) flags any account whose store holds more active rows than injection would surface, the no-event detector for drift.
3878
+ **Observability.** `[preference-write] op=reconcile … mode=<reinforce|update|contradict|new> similarity=…` — a stream of `mode=new` for subjects that already exist is the duplicate-minting signature. `[preference-inject] op=inject channel=<wa|tg|web> injected=<N> owner=<8> source=<owner|no-owner|owner-error|fetch-error>` — injection selects only the account owner's admin preferences, so `source=owner injected=0` is a genuinely empty owner, `source=no-owner` is an account with no seeded owner (injects nothing, a missed owner seeding), and `injected=0 source=owner` on an account whose owner holds rules is the write-only regression re-appearing. `[preference-audit] op=drift accountId=… storedActive=… injectedActive=… duplicateClusters=…` — the standing reconciliation audit (claude-session-manager, 5-minute tick) flags any account whose store holds more active rows than injection would surface, the no-event detector for drift.
3879
3879
 
3880
3880
  ---
3881
3881
 
@@ -4019,7 +4019,7 @@ The doctrine is observed at the storage primitive: writes to `:Person`, `:UserPr
4019
4019
 
4020
4020
  Two surfaces emit the lifecycle: agent-driven actions call `work-create`/`work-update`/`work-complete` over MCP (`work-create` accepts `kind`, the canonical `inputsProvided` call-shape record, `inputs` + `inputSchema` for the operator-meaningful form payload, and `raisedDuringConversationKey` to resolve the `RAISED_DURING` edge). Shell-driven actions wrap their script invocation in [platform/ui/app/lib/cloudflare-task-tracker.ts](../../../ui/app/lib/cloudflare-task-tracker.ts) (cloudflare is the first; installer / brand-publish / OAuth-login deferred). Both surfaces emit the same `[task] action-start|step|done` log lines so operators can grep one channel uniformly. Both also call the central `redactSecrets` primitive ([platform/lib/task-secrets/](../../../lib/task-secrets/)) to strip schema-tagged secret keys before persisting `inputs.<field>` props on the Task — see `.docs/neo4j.md § Audit Task input contract` for the contract that replaces per-kind allow-lists.
4021
4021
 
4022
- For estate-agent client work surfaced on the `/tasks` operator page, `work-create` also accepts optional `clientName`, `propertyAddress`, and `fee` (GBP major units) — plain props recorded on the `:Task`, omitted when absent. Billable time against a task is recorded separately as `:TimeEntry` nodes (`(:TimeEntry)-[:LOGGED_AGAINST]->(:Task)`): a running timer is the single account entry with `endedAt=null`, and each task carries a denormalized `secondsLogged` sum for list rendering. The platform records only the time; it computes no charges. See [`.docs/admin-tasks-page.md`](../../../../.docs/admin-tasks-page.md).
4022
+ For estate-agent client work surfaced on the `/tasks` operator page, `work-create` also accepts optional `clientName`, `propertyAddress`, and `fee` (GBP major units) — plain props recorded on the `:Task`, omitted when absent. Billable time against a task is recorded separately as `:TimeEntry` nodes (`(:TimeEntry)-[:LOGGED_AGAINST]->(:Task)`): a running timer is a `:TimeEntry` with `endedAt=null`, timers on different tasks run concurrently, and each task carries a denormalized `secondsLogged` sum for list rendering. A **manual** time correction (the `/tasks` page Edit/Reset controls, `task-time-adjust` route) sets `secondsLogged` to an absolute value and appends an append-only `:TimeAdjustment {adjustmentId, taskId, accountId, at, previousSeconds, newSeconds, delta, sourceSessionKey}` audit record — property-scoped, no edge. So the `[timeentry-census]` invariant is `secondsLogged == sum(TimeEntry.seconds) + sum(TimeAdjustment.delta)`, and an adjust is rejected while the task has an open `:TimeEntry`. The platform records only the time; it computes no charges. See [`.docs/admin-tasks-page.md`](../../../../.docs/admin-tasks-page.md).
4023
4023
 
4024
4024
  Two surfaces feed the gate. (1) **Workflow path:** `memory-write` accepts an optional `producedByTaskId` parameter. When set, an inbound `:PRODUCED` edge from that Task is composed into the write's relationships before the gate runs — the typical agent-side pattern is to call `work-create` at the start of an autonomous flow, capture `taskId`, and pass it as `producedByTaskId` on every subsequent `memory-write` for a gated label. The gate verifies Task and write share the same `accountId`; mismatch is rejected loud. (2) **Direct-ask path:** the admin server resolves the active `:AdminConversation`'s `sessionId` UUID and stamps it as `SESSION_NODE_ID` in the spawn env at PTY-spawn time. The same stamp propagates onto specialist subagent spawns the admin dispatches so listing-curator, content-producer, database-operator etc. inherit the same conversation anchor. The `contact-create` and `memory-write` wrappers call `injectConversationProvenance` (exported from [`@maxy/graph-write`](../../../lib/graph-write/src/conversation-provenance.ts)) which MATCHes `(c:Conversation {sessionId, accountId})` — account isolation is part of the natural key, not a separate gate — and prepends the synthetic `:PRODUCED` edge (composed by Neo4j elementId, which the helper reads off the MATCH). No agent-visible schema field changes. `memory-write` uses the env-stamp only as a fallback when `producedByTaskId` is unset; `contact-create` has no `producedByTaskId` parameter today and relies on the env-stamp alone. Autonomous (cron-driven) specialists with no parent conversation legitimately have no env-stamp; those must thread `producedByTaskId`.
4025
4025
 
@@ -97,6 +97,8 @@ The metadata pane surfaces two distinct identifier values. The three-id model wa
97
97
 
98
98
  **Re-seat mints a new id.** Model, mode, and effort are inception-only levers — claude reads them when a session is born and the `claude rc` daemon exposes no per-session switch — so changing one on an *existing* session forks it: `/api/admin/session-reseat` pre-mints a fresh `sessionId`, the manager `/rc-spawn` fork branch runs `--resume <old> --fork-session --session-id <new> --model <model>` (copying history into the new id), and the operator lands on the fork. The fork pins the chosen model and, when supplied, the chosen `permissionMode` (pushed as `--permission-mode`, one of the 5 composer-writable modes) and `effort` (`low|medium|high|xhigh`, merged into the per-spawn inline `--settings.effortLevel`); each is validated against its allowlist before reaching the argv (a present-but-disallowed value is a 400). Both `/chat`'s composer pickers and the dashboard's per-row Re-seat control drive this one fork, for webchat and WhatsApp/Telegram sessions alike. On `/chat`, every picker now carries the operator's *current* model, mode, and effort and overrides only the picked lever, so changing one never silently resets the other two (a non-writable current mode is omitted rather than rejected). When a re-seated webchat session runs in `default` ("Ask permissions") mode, a tool the agent attempts that needs approval surfaces an Allow/Deny prompt in `/chat` and blocks until the operator answers — over Claude Code's channel permission relay, the only interactive ask surface (WhatsApp stays text-only). The full write path is in [`admin-webchat-native-channel.md`](../../../.docs/admin-webchat-native-channel.md).
99
99
 
100
+ **The footer reads model/effort/turn-count from one shared JSONL resolver.** The `/chat` footer's model / effort / turn-count are the manager `GET /:sessionId/meta` enrichment, read from the session's JSONL tail. That read resolves the JSONL by the **same all-slug scan** the transcript uses — `findExistingJsonlForSessionId`, covering top-level and `archive/` under every project slug — not the single house-cwd slug. Previously the enrichment gated on the `spawnCwd` slug, so a sub-account session rc-spawned in a different cwd (its JSONL under a different slug) reported `model:null turns:0` while its transcript rendered; the transcript already scanned all slugs. `findSessionProjectDir` (the transcript resolver) delegates to the same function, so the two are one resolver — a session findable on disk is never reported `model:null`. The standing line is `[session-meta] op=enrich session=<id8> resolver=scan jsonl=<found|absent> model=… effort=… turns=…`.
101
+
100
102
  | Operator label | What it is | Manager wire field | Log key |
101
103
  |---|---|---|---|
102
104
  | `sessionId` | Claude's session. Two phases: bridge suffix (`session_xxx`, the URL segment in `claude.ai/code/<session_xxx>`, set when claude prints the `/remote-control` URL) and JSONL basename UUID (claude's intrinsic id on disk, bound when the first turn flushes the JSONL). Both phases coexist on a live row after URL capture; the manager wire emits the bridge form when set, falling back to the JSONL basename in the pre-URL-capture window. The resolver routes either phase to the same row, so callers never need to choose. | `sessionId` (collapsed from the earlier three-field surface `sessionId` + `claudeSessionId` + `bridgeSessionId`) | `sessionId=` |
@@ -56,7 +56,7 @@ either is a regression.
56
56
  |---|---|---|
57
57
  | `/session` | Admin cookie session: PIN-gated mint, validate, rotate. | `GET /`, `POST /` |
58
58
  | `/sessions` | Legacy admin-server conversation routes. No UI consumer remains after the ConversationsModal was retired; the surviving handlers are deletion candidates and not described here. | (legacy, no live caller) |
59
- | `/sidebar-sessions` | Sole data path for the sidebar Sessions list. One JSONL on disk equals one row. The row's delete button is the only way a row disappears. Each row carries `sessionId`, `title`, `startedAt`, `live`, `isSubagent`, `pid: number \| null` (basename of the matched `sessions/<pid>.json`), and `projectDir` (the directory holding the JSONL — consumed by the delete route). The payload also carries top-level `accountId` so the pane renders the full UUID label whose first ~8 chars prefix-match the truncated Remote Control daemon entry in claude.ai/code. The legacy `rcUrl` field is gone — the row's external-link affordance now POSTs `/session-rc-spawn` to start a fresh local `claude --remote-control <name> --session-id <sid>` PTY on every click. | `GET /` |
59
+ | `/sidebar-sessions` | Sole data path for the sidebar Sessions list. One JSONL on disk equals one row. The row's delete button is the only way a row disappears. Each row carries `sessionId`, `title`, `startedAt`, `live`, `isSubagent`, `pid: number \| null` (basename of the matched `sessions/<pid>.json`), and `projectDir` (the directory holding the JSONL — consumed by the delete route). The payload also carries top-level `accountId` so the pane renders the full UUID label whose first ~8 chars prefix-match the truncated Remote Control daemon entry in claude.ai/code. The legacy `rcUrl` field is gone — the row's external-link affordance now POSTs `/session-rc-spawn` to start a fresh local `claude --remote-control <name> --session-id <sid>` PTY on every click. **WhatsApp and Telegram sessions (admin + public) appear in this list**, colour-coded by channel (WhatsApp green `#25D366`, Telegram blue `#229ED9`, via a `conv-channel-*` left accent bar) and carrying the standard row kebab (Usage & cost, Reset/re-seat, Rename, Archive, Delete) so the operator can inspect and re-seat a channel session's params. Each row's `channel` is the authoritative `.meta.json` sidecar value (`whatsapp`/`telegram`/`webchat`/`browser`/null), not the JSONL-envelope channel. The only role/channel excluded from the list is `public`+`webchat` (its dedicated reader is the sole surface); the exclusion predicate is `isSessionListExcluded`, decoupled from the reader's inclusion predicate `isReaderChannelSession` (unchanged), so the WhatsApp/Telegram reader panels are unaffected — a store-only WhatsApp conversation with no session JSONL stays reader-only. Observability rides `[admin-sessions-list] … channelInList=whatsapp:<n>,telegram:<n>,webchat:<n> unknownChannel=<n> excludedPublicWebchat=<n>`. | `GET /` |
60
60
  | `/session-delete` | POST `{ sessionId }`. Thin proxy over the manager: POST `/:id/stop` (idempotent) then DELETE `/:id`. Sends **no** accountId. The manager resolves the session by scanning every slug under `projects/` for its JSONL (`slugForExistingJsonl`, covering `archive/`), exactly as the sidebar list enumerates rows, and gates on file existence alone — so any session the operator can see is deletable. This replaces the earlier accountId-derived slug and the `!row` term, which narrowed resolution below the list and left a session under a non-house slug with no watcher row (e.g. a legacy `-home-admin` session on a single-account install) listed yet un-deletable. Account scope is a view filter, not a delete gate — admin access is install-wide, so there is nothing to scope against. The sibling `/session-stop` (the standalone End control) resolves identically and also forwards no accountId. | `POST /` |
61
61
  | `/session-rc-spawn` | POST `{ sessionId?, name? }`. Fire-and-forget `claude --remote-control [name] [--session-id <sid>]`. Present `sessionId` resumes; absent starts a fresh session (also used by the sidebar's "New session" button — it no longer opens claude.ai/code directly). Proxies to the manager's `/rc-spawn`, which waits up to **60 s** (raised from 12 s) for the spawned PTY to bind and returns `{ spawnedPid, sessionId, bridgeSessionId, slug, outcome, reason }`. For a webchat-bound spawn (every admin-gated host's "New session", returning a same-origin `/chat?session=<id>` target — `resolveRcSpawnOutcome` → `sameOrigin:true`) the Sidebar navigates the **current** tab via `window.location.assign`, replacing the dashboard in place (back returns to it); only a claude.ai/code slug (`sameOrigin:false`, the bare-admin resume bridge, never a new-session outcome) navigates a separately-opened tab. On `timeout` or `spawn-failed` it shows an error modal (reason + sessionId) and **never** opens a bare claude.ai/code tab. The new process registers itself as its own Remote Control entry in claude.ai/code. | `POST /` |
62
62
  | `/claude-sessions` | **Spawn surface only**. `POST /` is the Sidebar new-session-with-prompt path, cookie-auth only (the recorder loopback caller was removed; LinkedIn ingest moved to `/rc-spawn`). The former UI-facing handlers (SSE row feed, list, resume, stop, rename, archive, delete, `/:id/meta`, `/:id/input`, `/:id/log`) were removed — the maxy dashboard no longer manages or displays sessions. | `POST /` |
@@ -69,7 +69,7 @@ either is a regression.
69
69
 
70
70
  **`/chat` composer transport button.** The button at the end of the input shows a **microphone** while the box is empty — tap it to record a voice note. Type a character, or attach a file, and the same button becomes the **send** arrow; while the agent is replying it becomes a **stop** square. So an empty composer offers voice, a composer with something to send offers send, and a running turn offers stop — one button, three states. The microphone-device chooser (which input to record from) stays in the row of icons below the box. This is the same on the admin `/chat`, the public chat, and the maxy-lite webchat.
71
71
 
72
- **`/chat` Conversations flyout + zero-sessions splash.** The admin `/chat` `HeaderMenu` carries a **Conversations** item (rendered only when `onOpenConversations` is wired, which scopes it to `/chat`) that opens an in-chat session-management pane (`app/chat/SessionList.tsx`) hosted by `chat/page.tsx` — distinct from, and sharing endpoints with, the `Sidebar` Sessions list (which stays hidden on narrow viewports). It enumerates the admin's own webchat sessions via `GET /api/admin/sidebar-sessions` (carries the per-row `live` marker and `archived` flag, install-wide), lists them newest-first with the live one marked and archived rows folded under a collapsible subsection, and offers per-row resume (`/chat?session=<id>`), rename, archive/unarchive, two-tap delete, an **End** control on the live row (`session-stop`), plus a New-session control (`session-rc-spawn`) and a copyable full id. The pane header carries the title and a top-right close (X) control that dismisses the flyout (no back-chevron/"Chat" label); the New-session control floats at the pane's bottom-left. When the canonical pointer is `known:false` (`canonical-empty`) and enumeration returns zero rows, the surface renders a splash (brand logo + New session) instead of a dead bootstrap thread; a freshly-spawned New session is `known:true`, so its greeting is preserved. Client breadcrumbs: `[chat-conversations] op=enumerate owned=<n>` and `op=action name=<open|rename|archive|delete|stop> sessionId=<id8>` (`op=action-failed … status=…` on a non-2xx). Detail lives in `.docs/admin-webchat-native-channel.md` ("Conversations flyout + zero-sessions splash").
72
+ **`/chat` Conversations flyout + zero-sessions splash.** The admin `/chat` `HeaderMenu` carries a **Conversations** item (rendered only when `onOpenConversations` is wired, which scopes it to `/chat`) that opens an in-chat session-management pane (`app/chat/SessionList.tsx`) hosted by `chat/page.tsx` — distinct from, and sharing endpoints with, the `Sidebar` Sessions list (which stays hidden on narrow viewports). It enumerates the admin's own webchat sessions via `GET /api/admin/sidebar-sessions` (carries the per-row `live` marker and `archived` flag, install-wide), **filtering out `channel:'whatsapp'`/`'telegram'` rows** (those are channel-bound, cannot be chatted in webchat, and live in the WhatsApp/Telegram reader panels; the same endpoint now also feeds the sidebar Sessions list, which does show them), lists the remainder newest-first with the live one marked and archived rows folded under a collapsible subsection, and offers per-row resume (`/chat?session=<id>`), rename, archive/unarchive, two-tap delete, an **End** control on the live row (`session-stop`), plus a New-session control (`session-rc-spawn`) and a copyable full id. The pane header carries the title and a top-right close (X) control that dismisses the flyout (no back-chevron/"Chat" label); the New-session control floats at the pane's bottom-left. When the canonical pointer is `known:false` (`canonical-empty`) and enumeration returns zero rows, the surface renders a splash (brand logo + New session) instead of a dead bootstrap thread; a freshly-spawned New session is `known:true`, so its greeting is preserved. Client breadcrumbs: `[chat-conversations] op=enumerate owned=<n>` and `op=action name=<open|rename|archive|delete|stop> sessionId=<id8>` (`op=action-failed … status=…` on a non-2xx). Detail lives in `.docs/admin-webchat-native-channel.md` ("Conversations flyout + zero-sessions splash").
73
73
 
74
74
  **`/chat` header always shows the account name.** The admin/operator `HeaderMenu` centre always renders the active account's name alongside the brand logo, on every chat surface, regardless of the active conversation — `headerTitle = businessName || brand.productName`. There is no per-conversation header title: the active conversation's label stays in the sidebar and the Conversations flyout (each resolves its own per-session title independently), never in the header. The account name is `LocalBusiness.name`, resolved by `fetchAccountName(accountId)` in `neo4j-store.ts` with **no theme gate** — unlike `fetchBranding` (which returns null unless a colour/logo property is also set, and stays the source for actual theming). The house account is seeded with `LocalBusiness.name = <brand productName>` (`seed-neo4j.sh`, coalesced ON MATCH so a re-seed backfills a name-less node and never clobbers a name the business-profile recorder set later), so the sub-account picker's house row shows the product name (e.g. "SiteDesk"), not the raw accountId UUID. When a name is absent the resolver emits `[branding] account-name-fallback accountId=<id8>… reason=no-name`, and the picker falls back to the accountId; the seed + backfill is the name source. The lite variant header is unchanged.
75
75
 
@@ -400,11 +400,11 @@ Standing rules the operator gives over a channel are `Preference` nodes. Two mec
400
400
 
401
401
  **Reconcile by meaning on write (`profile-update`).** The existence check is two-tier. First an exact `(category, key)` match — the classic fast path, unchanged. When that misses, a semantic-neighbour lookup runs over the `preference_embedding` vector index, scoped to the same `accountId` and `scope:'admin'`, non-trashed. If the nearest neighbour is at or above `SAME_SUBJECT_COSINE` (0.85), the write lands on that node through the existing reinforce/update/contradict curve instead of minting a duplicate; the matched neighbours are returned in the tool result so the agent can classify future writes against real prior state. The confidence constants are unchanged — only the join key widens, so a rephrasing under a new `key` no longer forks the rule and a correction demotes the prior node rather than adding a contradictory one.
402
402
 
403
- **Inject the active rule-set every turn (channel gateways).** The one definition of "this account's active rules" is `@maxy/active-rules`: a bounded, floor-filtered (`confidence >= 0.4`), confidence-ordered candidate query, then semantic de-confliction (one winner per subject at the same 0.85 cosine), capped to `INJECTION_RULE_CAP` (12). `profile-read` exposes this set as `activeRules`, and the three inbound gateways (`wa-gateway`, `telegram-gateway`, `webchat-gateway`) fetch it for the effective session account and attach it as `standingRules` on the inbound frame; the channel builders prepend it as a `## Standing rules` block above `## Context`. Both channel-server inbound paths carry the block: the single-target SSE loop parses the whole frame, and the unified multi-target door reader (`runDoorStream`,) reconstructs it through the shared `doorInboundPayload` seam whose `parseFrame` closures read `standingRules` off the frame. The read is by code, not agent discretion, so a rule set last week is applied this week regardless of how many times the session compacted. Telegram and webchat inject for `role:'admin'` only — a public visitor never receives operator rules.
403
+ **Inject the active rule-set every turn (channel gateways).** The one definition of "this account's active rules" is `@maxy/active-rules`: the account owner's admin-scope preferences (resolve the seeded `AdminUser {role:'owner'}`, then select through that owner's `UserProfile` via `HAS_PREFERENCE`), a bounded, floor-filtered (`confidence >= 0.4`), confidence-ordered candidate query, then semantic de-confliction (one winner per subject at the same 0.85 cosine), capped to `INJECTION_RULE_CAP` (12). A preference owned by a non-owner profile (an operator's, stranded on the operator's own profile while managing the account) is never injected; an account with no seeded owner injects nothing. `profile-read` exposes this same owner-scoped set as `activeRules`, and the three inbound gateways (`wa-gateway`, `telegram-gateway`, `webchat-gateway`) fetch it for the effective session account and attach it as `standingRules` on the inbound frame; the channel builders prepend it as a `## Standing rules` block above `## Context`. Both channel-server inbound paths carry the block: the single-target SSE loop parses the whole frame, and the unified multi-target door reader (`runDoorStream`,) reconstructs it through the shared `doorInboundPayload` seam whose `parseFrame` closures read `standingRules` off the frame. The read is by code, not agent discretion, so a rule set last week is applied this week regardless of how many times the session compacted. Telegram and webchat inject for `role:'admin'` only — a public visitor never receives operator rules.
404
404
 
405
405
  **Trust a scheduled turn's provenance, never an inbound's claim (channel-agent grounding).** A cron-dispatched agent turn reaches the same channel builders as a real inbound, so its own authorisation text would otherwise land inside `## Context` — the zone `REFRAME_INSTRUCTION` demotes to untrusted — and a security-cautious agent would distrust its own automation (the self-cancel loop). The dispatcher therefore stamps a `ScheduleProvenance` marker (owning account, event, owner userId, dispatch time) that the channel builders render as a `## Scheduled automation (platform-verified)` preamble **above** `## Context` (and above the rules block); the whatsapp wrap tag names the door `scheduler`. Both channel-server paths carry the marker — the single-target loop and the unified multi-target door reader (via the same `doorInboundPayload` seam that carries `standingRules`); the scheduler only dispatches to the whatsapp/telegram door, so a webchat frame never bears one. Because the marker is dispatcher-stamped and never derived from payload text, an inbound message cannot forge it — placement above `## Context` alone is not the trust signal, the dispatcher provenance is. The grounding binds to it in two places: one recipient-neutral clause added byte-identical to the three reframe instructions (a marker-bearing turn is authorised standing automation; a reconfiguration or re-enable is not intrusion), and the admin `IDENTITY.md` "Untrusted input" rule that the agent never states a platform-level authenticity or impersonation verdict it was not given (an unsourced "this is not from you" is its own inference, flagged, never asserted as the platform's). The amplifier that let one misread harden was the accreting seat: a recurring event resumed one long-lived session every firing. A subsequent change removes it — each firing runs a fresh per-firing seat (`scheduledSeatIdFor`, salted with the firing's eventId + dispatch time), so distrust cannot accrete across firings, and the self-cancel guard compares the cancelling `SESSION_ID` to the node's dispatcher-stamped `lastDispatchSessionId` rather than a recomputed deterministic seat. See `.docs/scheduling-agent-dispatch.md § Operator-provenance marker` and `§ Session model — fresh per firing`.
406
406
 
407
- **Observability.** `[preference-write] op=reconcile … mode=<reinforce|update|contradict|new> similarity=…` — a stream of `mode=new` for subjects that already exist is the duplicate-minting signature. `[preference-inject] op=inject channel=<wa|tg|web> injected=<N>` — `injected=0` on an account with stored rules is the write-only regression re-appearing. `[preference-audit] op=drift accountId=… storedActive=… injectedActive=… duplicateClusters=…` — the standing reconciliation audit (claude-session-manager, 5-minute tick) flags any account whose store holds more active rows than injection would surface, the no-event detector for drift.
407
+ **Observability.** `[preference-write] op=reconcile … mode=<reinforce|update|contradict|new> similarity=…` — a stream of `mode=new` for subjects that already exist is the duplicate-minting signature. `[preference-inject] op=inject channel=<wa|tg|web> injected=<N> owner=<8> source=<owner|no-owner|owner-error|fetch-error>` — injection selects only the account owner's admin preferences, so `source=owner injected=0` is a genuinely empty owner, `source=no-owner` is an account with no seeded owner (injects nothing, a missed owner seeding), and `injected=0 source=owner` on an account whose owner holds rules is the write-only regression re-appearing. `[preference-audit] op=drift accountId=… storedActive=… injectedActive=… duplicateClusters=…` — the standing reconciliation audit (claude-session-manager, 5-minute tick) flags any account whose store holds more active rows than injection would surface, the no-event detector for drift.
408
408
 
409
409
  ---
410
410
 
@@ -548,7 +548,7 @@ The doctrine is observed at the storage primitive: writes to `:Person`, `:UserPr
548
548
 
549
549
  Two surfaces emit the lifecycle: agent-driven actions call `work-create`/`work-update`/`work-complete` over MCP (`work-create` accepts `kind`, the canonical `inputsProvided` call-shape record, `inputs` + `inputSchema` for the operator-meaningful form payload, and `raisedDuringConversationKey` to resolve the `RAISED_DURING` edge). Shell-driven actions wrap their script invocation in [platform/ui/app/lib/cloudflare-task-tracker.ts](../../../ui/app/lib/cloudflare-task-tracker.ts) (cloudflare is the first; installer / brand-publish / OAuth-login deferred). Both surfaces emit the same `[task] action-start|step|done` log lines so operators can grep one channel uniformly. Both also call the central `redactSecrets` primitive ([platform/lib/task-secrets/](../../../lib/task-secrets/)) to strip schema-tagged secret keys before persisting `inputs.<field>` props on the Task — see `.docs/neo4j.md § Audit Task input contract` for the contract that replaces per-kind allow-lists.
550
550
 
551
- For estate-agent client work surfaced on the `/tasks` operator page, `work-create` also accepts optional `clientName`, `propertyAddress`, and `fee` (GBP major units) — plain props recorded on the `:Task`, omitted when absent. Billable time against a task is recorded separately as `:TimeEntry` nodes (`(:TimeEntry)-[:LOGGED_AGAINST]->(:Task)`): a running timer is the single account entry with `endedAt=null`, and each task carries a denormalized `secondsLogged` sum for list rendering. The platform records only the time; it computes no charges. See [`.docs/admin-tasks-page.md`](../../../../.docs/admin-tasks-page.md).
551
+ For estate-agent client work surfaced on the `/tasks` operator page, `work-create` also accepts optional `clientName`, `propertyAddress`, and `fee` (GBP major units) — plain props recorded on the `:Task`, omitted when absent. Billable time against a task is recorded separately as `:TimeEntry` nodes (`(:TimeEntry)-[:LOGGED_AGAINST]->(:Task)`): a running timer is a `:TimeEntry` with `endedAt=null`, timers on different tasks run concurrently, and each task carries a denormalized `secondsLogged` sum for list rendering. A **manual** time correction (the `/tasks` page Edit/Reset controls, `task-time-adjust` route) sets `secondsLogged` to an absolute value and appends an append-only `:TimeAdjustment {adjustmentId, taskId, accountId, at, previousSeconds, newSeconds, delta, sourceSessionKey}` audit record — property-scoped, no edge. So the `[timeentry-census]` invariant is `secondsLogged == sum(TimeEntry.seconds) + sum(TimeAdjustment.delta)`, and an adjust is rejected while the task has an open `:TimeEntry`. The platform records only the time; it computes no charges. See [`.docs/admin-tasks-page.md`](../../../../.docs/admin-tasks-page.md).
552
552
 
553
553
  Two surfaces feed the gate. (1) **Workflow path:** `memory-write` accepts an optional `producedByTaskId` parameter. When set, an inbound `:PRODUCED` edge from that Task is composed into the write's relationships before the gate runs — the typical agent-side pattern is to call `work-create` at the start of an autonomous flow, capture `taskId`, and pass it as `producedByTaskId` on every subsequent `memory-write` for a gated label. The gate verifies Task and write share the same `accountId`; mismatch is rejected loud. (2) **Direct-ask path:** the admin server resolves the active `:AdminConversation`'s `sessionId` UUID and stamps it as `SESSION_NODE_ID` in the spawn env at PTY-spawn time. The same stamp propagates onto specialist subagent spawns the admin dispatches so listing-curator, content-producer, database-operator etc. inherit the same conversation anchor. The `contact-create` and `memory-write` wrappers call `injectConversationProvenance` (exported from [`@maxy/graph-write`](../../../lib/graph-write/src/conversation-provenance.ts)) which MATCHes `(c:Conversation {sessionId, accountId})` — account isolation is part of the natural key, not a separate gate — and prepends the synthetic `:PRODUCED` edge (composed by Neo4j elementId, which the helper reads off the MATCH). No agent-visible schema field changes. `memory-write` uses the env-stamp only as a fallback when `producedByTaskId` is unset; `contact-create` has no `producedByTaskId` parameter today and relies on the env-stamp alone. Autonomous (cron-driven) specialists with no parent conversation legitimately have no env-stamp; those must thread `producedByTaskId`.
554
554