@rubytech/create-sitedesk-code 0.1.509 → 0.1.510
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.
- package/package.json +1 -1
- package/payload/platform/docs/superpowers/plans/2026-07-26-task-1902-account-owned-entry-declarations.md +555 -0
- package/payload/platform/docs/superpowers/plans/2026-07-26-task-2023-sidebar-sessions-async-reads.md +394 -0
- package/payload/platform/docs/superpowers/specs/2026-07-26-task-1902-account-owned-entry-declarations-design.md +232 -0
- package/payload/platform/docs/superpowers/specs/2026-07-26-task-2014-ui-suite-nondeterminism-design.md +169 -0
- package/payload/platform/plugins/admin/PLUGIN.md +1 -0
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +2 -2
- package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +6 -0
- package/payload/platform/plugins/business-assistant/PLUGIN.md +1 -0
- package/payload/platform/plugins/cloudflare/PLUGIN.md +1 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/schema-exposed-dirs.test.ts +21 -0
- package/payload/platform/plugins/docs/references/troubleshooting.md +1 -1
- package/payload/platform/plugins/scheduling/PLUGIN.md +1 -0
- package/payload/platform/scripts/__tests__/account-schema-owned-dirs.test.sh +112 -4
- package/payload/platform/scripts/lib/account-schema-owned-dirs.py +61 -32
- package/payload/platform/services/claude-session-manager/dist/account-dir-schema-reconcile.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/account-dir-schema-reconcile.js +17 -30
- package/payload/platform/services/claude-session-manager/dist/account-dir-schema-reconcile.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/config.d.ts +50 -4
- package/payload/platform/services/claude-session-manager/dist/config.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/config.js +58 -15
- package/payload/platform/services/claude-session-manager/dist/config.js.map +1 -1
- package/payload/server/maxy-edge.js +20 -10
- package/payload/server/public/assets/AdminLoginScreens-CWMpccrR.css.br +0 -0
- package/payload/server/public/assets/AdminLoginScreens-CWMpccrR.css.gz +0 -0
- package/payload/server/public/assets/AdminLoginScreens-bksN0XPe.js +1 -0
- package/payload/server/public/assets/AdminLoginScreens-bksN0XPe.js.br +0 -0
- package/payload/server/public/assets/AdminLoginScreens-bksN0XPe.js.gz +0 -0
- package/payload/server/public/assets/AdminShell-BMPEWNZk.js.br +0 -0
- package/payload/server/public/assets/AdminShell-BMPEWNZk.js.gz +0 -0
- package/payload/server/public/assets/_baseFor-Be3EKK3K.js.br +0 -0
- package/payload/server/public/assets/_baseFor-Be3EKK3K.js.gz +0 -0
- package/payload/server/public/assets/activity-ByWYF5dC.js.br +0 -0
- package/payload/server/public/assets/activity-ByWYF5dC.js.gz +0 -0
- package/payload/server/public/assets/{admin-CnUWbKcx.js → admin-DGr1HuPg.js} +1 -1
- package/payload/server/public/assets/admin-DGr1HuPg.js.br +0 -0
- package/payload/server/public/assets/admin-DGr1HuPg.js.gz +0 -0
- package/payload/server/public/assets/agents-C0rTfrYd.js.br +0 -0
- package/payload/server/public/assets/agents-C0rTfrYd.js.gz +0 -0
- package/payload/server/public/assets/arc-DaI5MLTq.js.br +0 -0
- package/payload/server/public/assets/arc-DaI5MLTq.js.gz +0 -0
- package/payload/server/public/assets/architecture-YZFGNWBL-DWZIX5aV.js.br +0 -0
- package/payload/server/public/assets/architectureDiagram-Q4EWVU46-CBGVUEN9.js.br +0 -0
- package/payload/server/public/assets/architectureDiagram-Q4EWVU46-CBGVUEN9.js.gz +0 -0
- package/payload/server/public/assets/array-X0JlPOfd.js.br +0 -0
- package/payload/server/public/assets/blockDiagram-DXYQGD6D-DS6dS4GZ.js.br +0 -0
- package/payload/server/public/assets/blockDiagram-DXYQGD6D-DS6dS4GZ.js.gz +0 -0
- package/payload/server/public/assets/browser-4tTaem8N.js.br +0 -0
- package/payload/server/public/assets/browser-4tTaem8N.js.gz +0 -0
- package/payload/server/public/assets/c4Diagram-AHTNJAMY-hBSBUZPt.js.br +0 -0
- package/payload/server/public/assets/c4Diagram-AHTNJAMY-hBSBUZPt.js.gz +0 -0
- package/payload/server/public/assets/calendar-BEjnkKG1.js.br +0 -0
- package/payload/server/public/assets/calendar-BEjnkKG1.js.gz +0 -0
- package/payload/server/public/assets/channel-BEokjAeE.js.br +0 -0
- package/payload/server/public/assets/chat-DBgzjDIE.js.br +0 -0
- package/payload/server/public/assets/chat-DBgzjDIE.js.gz +0 -0
- package/payload/server/public/assets/chevron-right-CQfPGsFb.js.br +0 -0
- package/payload/server/public/assets/chunk-2KRD3SAO-B6koUoB7.js.br +0 -0
- package/payload/server/public/assets/chunk-2KRD3SAO-B6koUoB7.js.gz +0 -0
- package/payload/server/public/assets/chunk-336JU56O-BF6BRh8u.js.br +0 -0
- package/payload/server/public/assets/chunk-336JU56O-BF6BRh8u.js.gz +0 -0
- package/payload/server/public/assets/chunk-426QAEUC-pbQ4PWBU.js.br +0 -0
- package/payload/server/public/assets/chunk-426QAEUC-pbQ4PWBU.js.gz +0 -0
- package/payload/server/public/assets/chunk-4BX2VUAB-7sO95xR2.js.br +0 -0
- package/payload/server/public/assets/chunk-4BX2VUAB-7sO95xR2.js.gz +0 -0
- package/payload/server/public/assets/chunk-4TB4RGXK-GCTndJYx.js.br +0 -0
- package/payload/server/public/assets/chunk-4TB4RGXK-GCTndJYx.js.gz +0 -0
- package/payload/server/public/assets/chunk-55IACEB6-ggIJf_TC.js.br +0 -0
- package/payload/server/public/assets/chunk-55IACEB6-ggIJf_TC.js.gz +0 -0
- package/payload/server/public/assets/chunk-5FUZZQ4R-DeGmkimt.js.br +0 -0
- package/payload/server/public/assets/chunk-5FUZZQ4R-DeGmkimt.js.gz +0 -0
- package/payload/server/public/assets/chunk-5PVQY5BW-aFIQKk_U.js.br +0 -0
- package/payload/server/public/assets/chunk-5PVQY5BW-aFIQKk_U.js.gz +0 -0
- package/payload/server/public/assets/chunk-67CJDMHE-LS8rUbii.js.br +2 -0
- package/payload/server/public/assets/chunk-67CJDMHE-LS8rUbii.js.gz +0 -0
- package/payload/server/public/assets/chunk-7N4EOEYR-JKOVngS0.js.br +2 -0
- package/payload/server/public/assets/chunk-7N4EOEYR-JKOVngS0.js.gz +0 -0
- package/payload/server/public/assets/chunk-AA7GKIK3-CoXnTVK6.js.br +0 -0
- package/payload/server/public/assets/chunk-AA7GKIK3-CoXnTVK6.js.gz +0 -0
- package/payload/server/public/assets/chunk-BSJP7CBP-CcsqObFg.js.br +0 -0
- package/payload/server/public/assets/chunk-BSJP7CBP-CcsqObFg.js.gz +0 -0
- package/payload/server/public/assets/chunk-CIAEETIT-CpsybR7q.js.br +0 -0
- package/payload/server/public/assets/chunk-CIAEETIT-CpsybR7q.js.gz +0 -0
- package/payload/server/public/assets/chunk-CltuBf4Z.js.br +0 -0
- package/payload/server/public/assets/chunk-CltuBf4Z.js.gz +0 -0
- package/payload/server/public/assets/chunk-EDXVE4YY-BZ2I9PSq.js.br +0 -0
- package/payload/server/public/assets/chunk-EDXVE4YY-BZ2I9PSq.js.gz +0 -0
- package/payload/server/public/assets/chunk-ENJZ2VHE-DbTwt6OZ.js.br +0 -0
- package/payload/server/public/assets/chunk-ENJZ2VHE-DbTwt6OZ.js.gz +0 -0
- package/payload/server/public/assets/chunk-FMBD7UC4-D27VnSCa.js.br +0 -0
- package/payload/server/public/assets/chunk-FMBD7UC4-D27VnSCa.js.gz +0 -0
- package/payload/server/public/assets/chunk-FOC6F5B3-C0tU2UFl.js.br +0 -0
- package/payload/server/public/assets/chunk-FOC6F5B3-C0tU2UFl.js.gz +0 -0
- package/payload/server/public/assets/chunk-ICPOFSXX-UPPu2OIq.js.br +0 -0
- package/payload/server/public/assets/chunk-ICPOFSXX-UPPu2OIq.js.gz +0 -0
- package/payload/server/public/assets/chunk-K5T4RW27-DIKxqyYw.js.br +0 -0
- package/payload/server/public/assets/chunk-K5T4RW27-DIKxqyYw.js.gz +0 -0
- package/payload/server/public/assets/chunk-KGLVRYIC-CiL4YVLo.js.br +0 -0
- package/payload/server/public/assets/chunk-KGLVRYIC-CiL4YVLo.js.gz +0 -0
- package/payload/server/public/assets/chunk-LIHQZDEY-ejKezVlD.js.br +0 -0
- package/payload/server/public/assets/chunk-LIHQZDEY-ejKezVlD.js.gz +0 -0
- package/payload/server/public/assets/chunk-ORNJ4GCN-Cc0vqc-D.js.br +0 -0
- package/payload/server/public/assets/chunk-ORNJ4GCN-Cc0vqc-D.js.gz +0 -0
- package/payload/server/public/assets/chunk-OYMX7WX6-Drxn1qG0.js.br +0 -0
- package/payload/server/public/assets/chunk-OYMX7WX6-Drxn1qG0.js.gz +0 -0
- package/payload/server/public/assets/chunk-QZHKN3VN-Dhlh7Pkk.js.br +0 -0
- package/payload/server/public/assets/chunk-QZHKN3VN-Dhlh7Pkk.js.gz +0 -0
- package/payload/server/public/assets/chunk-U2HBQHQK-DnbQIkRP.js.br +0 -0
- package/payload/server/public/assets/chunk-U2HBQHQK-DnbQIkRP.js.gz +0 -0
- package/payload/server/public/assets/chunk-X2U36JSP-CauTSVWg.js.br +0 -0
- package/payload/server/public/assets/chunk-X2U36JSP-CauTSVWg.js.gz +0 -0
- package/payload/server/public/assets/chunk-XPW4576I-DZqqB9Zi.js.br +0 -0
- package/payload/server/public/assets/chunk-XPW4576I-DZqqB9Zi.js.gz +0 -0
- package/payload/server/public/assets/chunk-YZCP3GAM-BGHo4Li-.js.br +0 -0
- package/payload/server/public/assets/chunk-YZCP3GAM-BGHo4Li-.js.gz +0 -0
- package/payload/server/public/assets/chunk-ZZ45TVLE-DWGJKPd7.js.br +0 -0
- package/payload/server/public/assets/chunk-ZZ45TVLE-DWGJKPd7.js.gz +0 -0
- package/payload/server/public/assets/classDiagram-6PBFFD2Q-D2z_ZV1x.js.br +0 -0
- package/payload/server/public/assets/classDiagram-6PBFFD2Q-D2z_ZV1x.js.gz +0 -0
- package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-BJwJoNEK.js.br +0 -0
- package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-BJwJoNEK.js.gz +0 -0
- package/payload/server/public/assets/clock-Dn6FHB51.js.br +0 -0
- package/payload/server/public/assets/clock-Dn6FHB51.js.gz +0 -0
- package/payload/server/public/assets/clone-Bn7XXVhr.js.br +0 -0
- package/payload/server/public/assets/cose-bilkent-S5V4N54A-D-95BzXL.js.br +0 -0
- package/payload/server/public/assets/cose-bilkent-S5V4N54A-D-95BzXL.js.gz +0 -0
- package/payload/server/public/assets/cytoscape.esm-CI2IWMLB.js.br +0 -0
- package/payload/server/public/assets/cytoscape.esm-CI2IWMLB.js.gz +0 -0
- package/payload/server/public/assets/dagre-BZYNBHvT.js.br +0 -0
- package/payload/server/public/assets/dagre-BZYNBHvT.js.gz +0 -0
- package/payload/server/public/assets/dagre-KV5264BT-DyU4HQLQ.js.br +0 -0
- package/payload/server/public/assets/dagre-KV5264BT-DyU4HQLQ.js.gz +0 -0
- package/payload/server/public/assets/data-D_e6Vdfd.js.br +0 -0
- package/payload/server/public/assets/data-D_e6Vdfd.js.gz +0 -0
- package/payload/server/public/assets/defaultLocale-B6RGN4id.js.br +0 -0
- package/payload/server/public/assets/defaultLocale-B6RGN4id.js.gz +0 -0
- package/payload/server/public/assets/diagram-5BDNPKRD-CCttJ9XA.js.br +0 -0
- package/payload/server/public/assets/diagram-5BDNPKRD-CCttJ9XA.js.gz +0 -0
- package/payload/server/public/assets/diagram-G4DWMVQ6-DvkpgSLO.js.br +0 -0
- package/payload/server/public/assets/diagram-G4DWMVQ6-DvkpgSLO.js.gz +0 -0
- package/payload/server/public/assets/diagram-MMDJMWI5-DHmW5Y9k.js.br +0 -0
- package/payload/server/public/assets/diagram-MMDJMWI5-DHmW5Y9k.js.gz +0 -0
- package/payload/server/public/assets/diagram-TYMM5635-C6T4hwzO.js.br +0 -0
- package/payload/server/public/assets/diagram-TYMM5635-C6T4hwzO.js.gz +0 -0
- package/payload/server/public/assets/dist-QvbmK1IP.js.br +0 -0
- package/payload/server/public/assets/dist-QvbmK1IP.js.gz +0 -0
- package/payload/server/public/assets/erDiagram-SMLLAGMA-5v_o7s3T.js.br +0 -0
- package/payload/server/public/assets/erDiagram-SMLLAGMA-5v_o7s3T.js.gz +0 -0
- package/payload/server/public/assets/file-text-DMKw3nIk.js.br +0 -0
- package/payload/server/public/assets/file-text-DMKw3nIk.js.gz +0 -0
- package/payload/server/public/assets/flatten-BtIc9jX7.js.br +0 -0
- package/payload/server/public/assets/flatten-BtIc9jX7.js.gz +0 -0
- package/payload/server/public/assets/flowDiagram-DWJPFMVM-3UhCDsjt.js.br +0 -0
- package/payload/server/public/assets/flowDiagram-DWJPFMVM-3UhCDsjt.js.gz +0 -0
- package/payload/server/public/assets/ganttDiagram-T4ZO3ILL-F2zgHIIQ.js.br +0 -0
- package/payload/server/public/assets/ganttDiagram-T4ZO3ILL-F2zgHIIQ.js.gz +0 -0
- package/payload/server/public/assets/gitGraph-7Q5UKJZL-C-FvNTHZ.js.br +0 -0
- package/payload/server/public/assets/gitGraphDiagram-UUTBAWPF-oQc1N3dT.js.br +0 -0
- package/payload/server/public/assets/gitGraphDiagram-UUTBAWPF-oQc1N3dT.js.gz +0 -0
- package/payload/server/public/assets/graph-C3vW8lvw.js.br +0 -0
- package/payload/server/public/assets/graph-C3vW8lvw.js.gz +0 -0
- package/payload/server/public/assets/graph-labels-BZgd0L6n.js.br +0 -0
- package/payload/server/public/assets/graph-labels-BZgd0L6n.js.gz +0 -0
- package/payload/server/public/assets/graphlib-DY4-AT2Q.js.br +0 -0
- package/payload/server/public/assets/graphlib-DY4-AT2Q.js.gz +0 -0
- package/payload/server/public/assets/info-OMHHGYJF-BjLPgHAC.js.br +0 -0
- package/payload/server/public/assets/infoDiagram-42DDH7IO-CXNKLTui.js.br +0 -0
- package/payload/server/public/assets/infoDiagram-42DDH7IO-CXNKLTui.js.gz +0 -0
- package/payload/server/public/assets/init-BmUWJJHz.js.br +0 -0
- package/payload/server/public/assets/init-BmUWJJHz.js.gz +0 -0
- package/payload/server/public/assets/isEmpty-Bu4fZ8Kf.js.br +0 -0
- package/payload/server/public/assets/isEmpty-Bu4fZ8Kf.js.gz +0 -0
- package/payload/server/public/assets/ishikawaDiagram-UXIWVN3A-wJhSOkuw.js.br +0 -0
- package/payload/server/public/assets/ishikawaDiagram-UXIWVN3A-wJhSOkuw.js.gz +0 -0
- package/payload/server/public/assets/journeyDiagram-VCZTEJTY-DgadC6Pz.js.br +0 -0
- package/payload/server/public/assets/journeyDiagram-VCZTEJTY-DgadC6Pz.js.gz +0 -0
- package/payload/server/public/assets/kanban-definition-6JOO6SKY-Bs4nQqXI.js.br +0 -0
- package/payload/server/public/assets/kanban-definition-6JOO6SKY-Bs4nQqXI.js.gz +0 -0
- package/payload/server/public/assets/katex-BjZuzSQn.js.br +0 -0
- package/payload/server/public/assets/katex-BjZuzSQn.js.gz +0 -0
- package/payload/server/public/assets/line-Bnmxy1hO.js.br +0 -0
- package/payload/server/public/assets/line-Bnmxy1hO.js.gz +0 -0
- package/payload/server/public/assets/linear-RvwZxFzh.js.br +0 -0
- package/payload/server/public/assets/linear-RvwZxFzh.js.gz +0 -0
- package/payload/server/public/assets/maximize-2-la3IBA3M.js.br +0 -0
- package/payload/server/public/assets/maximize-2-la3IBA3M.js.gz +0 -0
- package/payload/server/public/assets/mermaid-parser.core-DeNV8RtA.js.br +0 -0
- package/payload/server/public/assets/mermaid-parser.core-DeNV8RtA.js.gz +0 -0
- package/payload/server/public/assets/mermaid.core-BU9IiSCP.js.br +0 -0
- package/payload/server/public/assets/mermaid.core-BU9IiSCP.js.gz +0 -0
- package/payload/server/public/assets/mindmap-definition-QFDTVHPH-D1PMUxxm.js.br +0 -0
- package/payload/server/public/assets/mindmap-definition-QFDTVHPH-D1PMUxxm.js.gz +0 -0
- package/payload/server/public/assets/{operator-RRRdqoRQ.js → operator-D9NgVL9_.js} +1 -1
- package/payload/server/public/assets/operator-D9NgVL9_.js.br +0 -0
- package/payload/server/public/assets/operator-D9NgVL9_.js.gz +0 -0
- package/payload/server/public/assets/ordinal-C2TEWKNu.js.br +0 -0
- package/payload/server/public/assets/ordinal-C2TEWKNu.js.gz +0 -0
- package/payload/server/public/assets/packet-4T2RLAQJ-BBAZASfb.js.br +0 -0
- package/payload/server/public/assets/page-BWFHRIAH.js.br +0 -0
- package/payload/server/public/assets/page-BWFHRIAH.js.gz +0 -0
- package/payload/server/public/assets/page-DC11gesX.js.br +0 -0
- package/payload/server/public/assets/page-DC11gesX.js.gz +0 -0
- package/payload/server/public/assets/path-CoPyR7c2.js.br +0 -0
- package/payload/server/public/assets/path-CoPyR7c2.js.gz +0 -0
- package/payload/server/public/assets/pdf-render-C257cidb.js.br +0 -0
- package/payload/server/public/assets/pdf-render-C257cidb.js.gz +0 -0
- package/payload/server/public/assets/pdf.worker.min-yatZIOMy.mjs.br +0 -0
- package/payload/server/public/assets/pdf.worker.min-yatZIOMy.mjs.gz +0 -0
- package/payload/server/public/assets/pie-ZZUOXDRM-CCJUnMeG.js.br +0 -0
- package/payload/server/public/assets/pieDiagram-DEJITSTG-BzdNzoKA.js.br +0 -0
- package/payload/server/public/assets/pieDiagram-DEJITSTG-BzdNzoKA.js.gz +0 -0
- package/payload/server/public/assets/preload-helper-BQrMkyGX.js.br +0 -0
- package/payload/server/public/assets/preload-helper-BQrMkyGX.js.gz +0 -0
- package/payload/server/public/assets/public-DQbbLHQN.js.br +0 -0
- package/payload/server/public/assets/public-DQbbLHQN.js.gz +0 -0
- package/payload/server/public/assets/quadrantDiagram-34T5L4WZ-oFr1MJRO.js.br +0 -0
- package/payload/server/public/assets/quadrantDiagram-34T5L4WZ-oFr1MJRO.js.gz +0 -0
- package/payload/server/public/assets/radar-PYXPWWZC-BsRU8Aep.js.br +0 -0
- package/payload/server/public/assets/reduce-BGpN5lgn.js.br +0 -0
- package/payload/server/public/assets/reduce-BGpN5lgn.js.gz +0 -0
- package/payload/server/public/assets/requirementDiagram-MS252O5E-C9QBQxDh.js.br +0 -0
- package/payload/server/public/assets/requirementDiagram-MS252O5E-C9QBQxDh.js.gz +0 -0
- package/payload/server/public/assets/rotate-ccw-OQD5si8N.js.br +0 -0
- package/payload/server/public/assets/rotate-ccw-OQD5si8N.js.gz +0 -0
- package/payload/server/public/assets/rough.esm-jSt7UMyi.js.br +0 -0
- package/payload/server/public/assets/rough.esm-jSt7UMyi.js.gz +0 -0
- package/payload/server/public/assets/routines-asrBGZUR.js.br +0 -0
- package/payload/server/public/assets/routines-asrBGZUR.js.gz +0 -0
- package/payload/server/public/assets/sankeyDiagram-XADWPNL6-c_y3bvVv.js.br +0 -0
- package/payload/server/public/assets/sankeyDiagram-XADWPNL6-c_y3bvVv.js.gz +0 -0
- package/payload/server/public/assets/sequenceDiagram-FGHM5R23-YwlCBPHE.js.br +0 -0
- package/payload/server/public/assets/sequenceDiagram-FGHM5R23-YwlCBPHE.js.gz +0 -0
- package/payload/server/public/assets/skills-D4ECwoxa.js.br +0 -0
- package/payload/server/public/assets/skills-D4ECwoxa.js.gz +0 -0
- package/payload/server/public/assets/src-C2WwTWSY.js.br +0 -0
- package/payload/server/public/assets/src-C2WwTWSY.js.gz +0 -0
- package/payload/server/public/assets/stateDiagram-FHFEXIEX-BFE3e-FM.js.br +0 -0
- package/payload/server/public/assets/stateDiagram-FHFEXIEX-BFE3e-FM.js.gz +0 -0
- package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-CpjB4ykI.js.br +4 -0
- package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-CpjB4ykI.js.gz +0 -0
- package/payload/server/public/assets/tasks-U3QdbUsO.js.br +0 -0
- package/payload/server/public/assets/tasks-U3QdbUsO.js.gz +0 -0
- package/payload/server/public/assets/time-entry-format-BgdTKgYr.js.br +0 -0
- package/payload/server/public/assets/time-entry-format-BgdTKgYr.js.gz +0 -0
- package/payload/server/public/assets/timeline-definition-GMOUNBTQ-azqhmRwZ.js.br +0 -0
- package/payload/server/public/assets/timeline-definition-GMOUNBTQ-azqhmRwZ.js.gz +0 -0
- package/payload/server/public/assets/treeView-SZITEDCU-BDzhKA6v.js.br +0 -0
- package/payload/server/public/assets/treemap-W4RFUUIX-epHpdeWa.js.br +0 -0
- package/payload/server/public/assets/triangle-alert-Ck_3VhT9.js.br +0 -0
- package/payload/server/public/assets/triangle-alert-Ck_3VhT9.js.gz +0 -0
- package/payload/server/public/assets/useCopyFeedback-BdUwyNpa.js.br +0 -0
- package/payload/server/public/assets/useCopyFeedback-BdUwyNpa.js.gz +0 -0
- package/payload/server/public/assets/useSubAccountSwitcher-CJKjEPNN.js.br +0 -0
- package/payload/server/public/assets/useSubAccountSwitcher-CJKjEPNN.js.gz +0 -0
- package/payload/server/public/assets/useSubAccountSwitcher-DOWQXDT8.css.br +0 -0
- package/payload/server/public/assets/useSubAccountSwitcher-DOWQXDT8.css.gz +0 -0
- package/payload/server/public/assets/useVoiceRecorder-ChnBrzbZ.js.br +0 -0
- package/payload/server/public/assets/useVoiceRecorder-ChnBrzbZ.js.gz +0 -0
- package/payload/server/public/assets/vennDiagram-DHZGUBPP-Da2RfFs8.js.br +0 -0
- package/payload/server/public/assets/vennDiagram-DHZGUBPP-Da2RfFs8.js.gz +0 -0
- package/payload/server/public/assets/wardley-RL74JXVD-DNSZCrxV.js.br +0 -0
- package/payload/server/public/assets/wardleyDiagram-NUSXRM2D-HdrdBYES.js.br +0 -0
- package/payload/server/public/assets/wardleyDiagram-NUSXRM2D-HdrdBYES.js.gz +0 -0
- package/payload/server/public/assets/wrench-Cpee3C3J.js.br +2 -0
- package/payload/server/public/assets/wrench-Cpee3C3J.js.gz +0 -0
- package/payload/server/public/assets/xychartDiagram-5P7HB3ND-DMk6rTcQ.js.br +0 -0
- package/payload/server/public/assets/xychartDiagram-5P7HB3ND-DMk6rTcQ.js.gz +0 -0
- package/payload/server/public/index.html +2 -2
- package/payload/server/public/operator.html +2 -2
- package/payload/server/server.js +524 -503
- package/payload/server/public/assets/AdminLoginScreens-8Jk7hnWH.js +0 -1
package/payload/platform/docs/superpowers/plans/2026-07-26-task-2023-sidebar-sessions-async-reads.md
ADDED
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
# Task 2023 — sidebar-sessions async reads Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
4
|
+
|
|
5
|
+
**Goal:** Stop `computeSessionList` from blocking the one event loop for the whole duration of its filesystem walk, without changing what the endpoint returns.
|
|
6
|
+
|
|
7
|
+
**Architecture:** The route already sits behind the Task 1986 per-account cache and its compute is already `async` and awaited. The block is not the awaiting; it is that every read inside the compute is synchronous, so the loop cannot run anything else from the first `readFileSync` to the last. Replacing the per-row reads with `node:fs/promises` gives the loop a yield point per row. Directory enumeration is left alone on measurement, not preference (see Global Constraints).
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** TypeScript, Node 22, Hono, vitest.
|
|
10
|
+
|
|
11
|
+
## Global Constraints
|
|
12
|
+
|
|
13
|
+
- **Measured cost split, maxy 2026-07-26, 52 files / 133.9 MB / 38,042 lines, two identical runs:** `enumMs=1`, `readMs=284.5` then `277.4`, `parseMs=212.3` then `216.6`.
|
|
14
|
+
- **The four `readdirSync` calls cost 1 ms and are NOT converted.** `enumerateJsonls` is imported by six other server modules; forking it for 1 ms would trade a measurable regression risk for no measurable gain. This is a deliberate deviation from the task file's problem statement, which names lines 216, 227, 240 and 255 alongside the reads that do cost.
|
|
15
|
+
- **JSON parsing is ~215 ms of pure CPU and no async change removes it.** The deliverable is that it stops arriving as one 215 ms block and arrives as ~52 chunks separated by I/O yields. Do not claim the parse cost is eliminated.
|
|
16
|
+
- **Reads stay sequential.** The current loop holds one `body` at a time and lets it go out of scope each iteration. `Promise.all` over 52 files would hold 133.9 MB at once. Sequential `await` yields the loop without changing the memory profile.
|
|
17
|
+
- **No behaviour change.** Response shape, field values, ordering, census log lines and cache states are all byte-identical.
|
|
18
|
+
- **Existing sync exports keep their signatures.** `enumerateJsonls`, `loadUserTitles`, `readSidecarMeta` and `readBridgeIds` are consumed by `server/index.ts`, `public-reader.ts`, `webchat.ts`, `whatsapp-reader.ts`, `files.ts` and two test files. None of those are in scope.
|
|
19
|
+
- **One parser per format.** Where a sync and an async reader both exist, they share a single exported pure parse function. Two copies of a parse body is a drift registry and is not acceptable.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## File Structure
|
|
24
|
+
|
|
25
|
+
- `server/routes/admin/sidebar-sessions.ts` — the only production file modified. Gains `parseUserTitles`, `parseSidecarMeta`, `loadUserTitlesAsync`, `readSidecarMetaAsync`; `computeSessionList`'s row loop switches to `fs/promises`.
|
|
26
|
+
- `server/routes/admin/__tests__/sidebar-sessions-async-reads.test.ts` — new, holds this task's own tests.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
### Task 1: Split the two parsers out of their sync readers
|
|
31
|
+
|
|
32
|
+
**Files:**
|
|
33
|
+
- Modify: `platform/ui/server/routes/admin/sidebar-sessions.ts:308-335` (`loadUserTitles`), `:371-402` (`readSidecarMeta`)
|
|
34
|
+
- Test: `platform/ui/server/routes/admin/__tests__/sidebar-sessions-async-reads.test.ts`
|
|
35
|
+
|
|
36
|
+
**Interfaces:**
|
|
37
|
+
- Produces: `parseUserTitles(raw: string): Map<string, string>` and `parseSidecarMeta(raw: string): SidecarMeta`. Both take the file's raw text and never touch the filesystem. Both degrade to empty/null on a parse failure or wrong shape, exactly as the current readers do.
|
|
38
|
+
- Consumes: nothing.
|
|
39
|
+
|
|
40
|
+
- [ ] **Step 1: Write the failing test**
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import { describe, it, expect } from 'vitest'
|
|
44
|
+
import { parseUserTitles, parseSidecarMeta } from '../sidebar-sessions'
|
|
45
|
+
|
|
46
|
+
describe('parseUserTitles', () => {
|
|
47
|
+
it('keeps a trimmed string title and drops every malformed entry', () => {
|
|
48
|
+
const raw = JSON.stringify({
|
|
49
|
+
good: { title: ' renamed ' },
|
|
50
|
+
blankTitle: { title: ' ' },
|
|
51
|
+
wrongType: { title: 42 },
|
|
52
|
+
notAnObject: 'nope',
|
|
53
|
+
'': { title: 'no id' },
|
|
54
|
+
})
|
|
55
|
+
const out = parseUserTitles(raw)
|
|
56
|
+
expect(out.get('good')).toBe('renamed')
|
|
57
|
+
expect([...out.keys()]).toEqual(['good'])
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('returns an empty map for unparseable or non-object JSON', () => {
|
|
61
|
+
expect(parseUserTitles('{not json').size).toBe(0)
|
|
62
|
+
expect(parseUserTitles('[1,2,3]').size).toBe(0)
|
|
63
|
+
expect(parseUserTitles('null').size).toBe(0)
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
describe('parseSidecarMeta', () => {
|
|
68
|
+
it('reads every field and filters non-string bridgeIds', () => {
|
|
69
|
+
const raw = JSON.stringify({
|
|
70
|
+
bridgeIds: ['a', '', 7, 'b'],
|
|
71
|
+
role: 'admin',
|
|
72
|
+
channel: 'webchat',
|
|
73
|
+
senderId: '+447700900000',
|
|
74
|
+
startedAt: '2026-07-26T00:00:00.000Z',
|
|
75
|
+
personId: 'p1',
|
|
76
|
+
adminUserId: 'u1',
|
|
77
|
+
visitorId: 'v1',
|
|
78
|
+
agentSlug: 'slug',
|
|
79
|
+
accountId: 'acct',
|
|
80
|
+
})
|
|
81
|
+
expect(parseSidecarMeta(raw)).toEqual({
|
|
82
|
+
bridgeIds: ['a', 'b'],
|
|
83
|
+
role: 'admin',
|
|
84
|
+
channel: 'webchat',
|
|
85
|
+
senderId: '+447700900000',
|
|
86
|
+
startedAt: '2026-07-26T00:00:00.000Z',
|
|
87
|
+
personId: 'p1',
|
|
88
|
+
adminUserId: 'u1',
|
|
89
|
+
visitorId: 'v1',
|
|
90
|
+
agentSlug: 'slug',
|
|
91
|
+
accountId: 'acct',
|
|
92
|
+
})
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('returns the empty shape for unparseable JSON', () => {
|
|
96
|
+
expect(parseSidecarMeta('{not json')).toEqual({
|
|
97
|
+
bridgeIds: [], role: null, channel: null, senderId: null, startedAt: null,
|
|
98
|
+
personId: null, adminUserId: null, visitorId: null, agentSlug: null, accountId: null,
|
|
99
|
+
})
|
|
100
|
+
})
|
|
101
|
+
})
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
105
|
+
|
|
106
|
+
Run: `cd maxy-code/platform/ui && npx vitest run server/routes/admin/__tests__/sidebar-sessions-async-reads.test.ts`
|
|
107
|
+
Expected: FAIL — `parseUserTitles is not a function` (the export does not exist yet).
|
|
108
|
+
|
|
109
|
+
- [ ] **Step 3: Write minimal implementation**
|
|
110
|
+
|
|
111
|
+
Replace the body of `loadUserTitles` from the `JSON.parse` onward with a call to a new exported pure function, and do the same for `readSidecarMeta`. The sync readers keep their exact signatures and their `readFileSync` + catch.
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
/** Pure half of {@link loadUserTitles}: parse the on-disk `session-titles.json`
|
|
115
|
+
* text. Shared with the async reader so one format has one parser. */
|
|
116
|
+
export function parseUserTitles(raw: string): Map<string, string> {
|
|
117
|
+
const out = new Map<string, string>()
|
|
118
|
+
let parsed: unknown
|
|
119
|
+
try { parsed = JSON.parse(raw) } catch { return out }
|
|
120
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return out
|
|
121
|
+
for (const [sessionId, value] of Object.entries(parsed as Record<string, unknown>)) {
|
|
122
|
+
if (!sessionId) continue
|
|
123
|
+
if (!value || typeof value !== 'object') continue
|
|
124
|
+
const v = value as { title?: unknown }
|
|
125
|
+
if (typeof v.title !== 'string') continue
|
|
126
|
+
const trimmed = v.title.trim()
|
|
127
|
+
if (!trimmed) continue
|
|
128
|
+
out.set(sessionId, trimmed)
|
|
129
|
+
}
|
|
130
|
+
return out
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function loadUserTitles(accountDir: string | null): Map<string, string> {
|
|
134
|
+
if (!accountDir) return new Map()
|
|
135
|
+
let raw: string
|
|
136
|
+
try { raw = readFileSync(join(accountDir, 'session-titles.json'), 'utf8') } catch { return new Map() }
|
|
137
|
+
return parseUserTitles(raw)
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
// A factory, not a shared constant. `{ ...CONST }` would copy the REFERENCE to
|
|
143
|
+
// one `bridgeIds` array into every empty result, so a single caller mutating it
|
|
144
|
+
// would corrupt every future empty sidecar in the process.
|
|
145
|
+
function emptySidecarMeta(): SidecarMeta {
|
|
146
|
+
return {
|
|
147
|
+
bridgeIds: [], role: null, channel: null, senderId: null, startedAt: null,
|
|
148
|
+
personId: null, adminUserId: null, visitorId: null, agentSlug: null, accountId: null,
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Pure half of {@link readSidecarMeta}: parse a `.meta.json` sidecar's text.
|
|
153
|
+
* Shared with the async reader so one format has one parser. */
|
|
154
|
+
export function parseSidecarMeta(raw: string): SidecarMeta {
|
|
155
|
+
let parsed: unknown
|
|
156
|
+
try { parsed = JSON.parse(raw) } catch { return emptySidecarMeta() }
|
|
157
|
+
if (!parsed || typeof parsed !== 'object') return emptySidecarMeta()
|
|
158
|
+
const r = parsed as { bridgeIds?: unknown; role?: unknown; channel?: unknown; senderId?: unknown; startedAt?: unknown; personId?: unknown; adminUserId?: unknown; visitorId?: unknown; agentSlug?: unknown; accountId?: unknown }
|
|
159
|
+
const bridgeIds = Array.isArray(r.bridgeIds)
|
|
160
|
+
? r.bridgeIds.filter((b): b is string => typeof b === 'string' && b.length > 0)
|
|
161
|
+
: []
|
|
162
|
+
return {
|
|
163
|
+
bridgeIds,
|
|
164
|
+
role: typeof r.role === 'string' ? r.role : null,
|
|
165
|
+
channel: typeof r.channel === 'string' ? r.channel : null,
|
|
166
|
+
senderId: typeof r.senderId === 'string' ? r.senderId : null,
|
|
167
|
+
startedAt: typeof r.startedAt === 'string' ? r.startedAt : null,
|
|
168
|
+
personId: typeof r.personId === 'string' ? r.personId : null,
|
|
169
|
+
adminUserId: typeof r.adminUserId === 'string' ? r.adminUserId : null,
|
|
170
|
+
visitorId: typeof r.visitorId === 'string' ? r.visitorId : null,
|
|
171
|
+
agentSlug: typeof r.agentSlug === 'string' ? r.agentSlug : null,
|
|
172
|
+
accountId: typeof r.accountId === 'string' ? r.accountId : null,
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function readSidecarMeta(metaPath: string): SidecarMeta {
|
|
177
|
+
let raw: string
|
|
178
|
+
try { raw = readFileSync(metaPath, 'utf8') } catch { return emptySidecarMeta() }
|
|
179
|
+
return parseSidecarMeta(raw)
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
- [ ] **Step 4: Run tests to verify they pass**
|
|
184
|
+
|
|
185
|
+
Run: `cd maxy-code/platform/ui && npx vitest run server/routes/admin/__tests__/sidebar-sessions-async-reads.test.ts server/routes/admin/__tests__/sidebar-sessions.test.ts server/routes/admin/__tests__/resolve-title.test.ts`
|
|
186
|
+
Expected: PASS on all three files. The two pre-existing files are the regression guard on the sync readers whose bodies just moved.
|
|
187
|
+
|
|
188
|
+
- [ ] **Step 5: Commit**
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
git add maxy-code/platform/ui/server/routes/admin/sidebar-sessions.ts maxy-code/platform/ui/server/routes/admin/__tests__/sidebar-sessions-async-reads.test.ts
|
|
192
|
+
git commit -m "refactor(2023): split the sidecar and user-title parsers out of their sync readers"
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
### Task 2: Add the async readers and switch the row loop onto them
|
|
198
|
+
|
|
199
|
+
**Files:**
|
|
200
|
+
- Modify: `platform/ui/server/routes/admin/sidebar-sessions.ts:70` (imports), `:572` (`loadUserTitles` call site), `:642-655` (row loop reads)
|
|
201
|
+
- Test: `platform/ui/server/routes/admin/__tests__/sidebar-sessions-async-reads.test.ts`
|
|
202
|
+
|
|
203
|
+
**Interfaces:**
|
|
204
|
+
- Consumes: `parseUserTitles`, `parseSidecarMeta`, `emptySidecarMeta` from Task 1.
|
|
205
|
+
- Produces: `loadUserTitlesAsync(accountDir: string | null): Promise<Map<string, string>>` and `readSidecarMetaAsync(metaPath: string): Promise<SidecarMeta>`. Same degradation rules as their sync twins.
|
|
206
|
+
|
|
207
|
+
- [ ] **Step 1: Write the failing test**
|
|
208
|
+
|
|
209
|
+
The load-bearing assertion is that the loop yields. A test that only checks the returned rows would pass just as well against the synchronous version, which is the dead-signal case of Task 1730. So the test races a timer against the compute: with synchronous reads the timer cannot fire until the walk finishes; with async reads it fires during it.
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
213
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'
|
|
214
|
+
import { tmpdir } from 'node:os'
|
|
215
|
+
import { join } from 'node:path'
|
|
216
|
+
import { loadUserTitlesAsync, readSidecarMetaAsync } from '../sidebar-sessions'
|
|
217
|
+
|
|
218
|
+
let dir: string
|
|
219
|
+
beforeEach(() => { dir = mkdtempSync(join(tmpdir(), 'sbs-async-')) })
|
|
220
|
+
afterEach(() => { rmSync(dir, { recursive: true, force: true }) })
|
|
221
|
+
|
|
222
|
+
describe('loadUserTitlesAsync', () => {
|
|
223
|
+
it('reads and parses the same file as the sync twin', async () => {
|
|
224
|
+
writeFileSync(join(dir, 'session-titles.json'), JSON.stringify({ s1: { title: 'renamed' } }))
|
|
225
|
+
expect((await loadUserTitlesAsync(dir)).get('s1')).toBe('renamed')
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
it('returns an empty map for a null accountDir and for a missing file', async () => {
|
|
229
|
+
expect((await loadUserTitlesAsync(null)).size).toBe(0)
|
|
230
|
+
expect((await loadUserTitlesAsync(join(dir, 'absent'))).size).toBe(0)
|
|
231
|
+
})
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
describe('readSidecarMetaAsync', () => {
|
|
235
|
+
it('reads and parses the same file as the sync twin', async () => {
|
|
236
|
+
const p = join(dir, 'x.meta.json')
|
|
237
|
+
writeFileSync(p, JSON.stringify({ role: 'admin', channel: 'webchat' }))
|
|
238
|
+
const meta = await readSidecarMetaAsync(p)
|
|
239
|
+
expect(meta.role).toBe('admin')
|
|
240
|
+
expect(meta.channel).toBe('webchat')
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
it('returns the empty shape for a missing file', async () => {
|
|
244
|
+
expect((await readSidecarMetaAsync(join(dir, 'absent.meta.json'))).bridgeIds).toEqual([])
|
|
245
|
+
})
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
describe('the row loop yields to the event loop', () => {
|
|
249
|
+
it('lets a pending timer fire while the walk is still running', async () => {
|
|
250
|
+
const root = join(dir, 'projects', 'slug')
|
|
251
|
+
mkdirSync(root, { recursive: true })
|
|
252
|
+
// Bodies large enough that the walk cannot complete inside one tick.
|
|
253
|
+
const line = JSON.stringify({ type: 'user', message: { role: 'user', content: 'x'.repeat(400) } })
|
|
254
|
+
for (let i = 0; i < 40; i++) {
|
|
255
|
+
const id = `0000000${i.toString().padStart(2, '0')}-0000-4000-8000-000000000000`
|
|
256
|
+
writeFileSync(join(root, `${id}.jsonl`), `${line}\n`.repeat(500))
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
let firedDuringWalk = false
|
|
260
|
+
const walk = readAllBodiesForTest(join(dir, 'projects'))
|
|
261
|
+
setTimeout(() => { firedDuringWalk = true }, 0)
|
|
262
|
+
await walk
|
|
263
|
+
expect(firedDuringWalk).toBe(true)
|
|
264
|
+
})
|
|
265
|
+
})
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
269
|
+
|
|
270
|
+
Run: `cd maxy-code/platform/ui && npx vitest run server/routes/admin/__tests__/sidebar-sessions-async-reads.test.ts`
|
|
271
|
+
Expected: FAIL — `loadUserTitlesAsync is not a function`, and `readAllBodiesForTest is not defined`.
|
|
272
|
+
|
|
273
|
+
- [ ] **Step 3: Write minimal implementation**
|
|
274
|
+
|
|
275
|
+
Add the promises import beside the existing sync one, add the two async readers, and export the loop's read half as `readAllBodiesForTest` so the yield test drives the shipped code rather than a hand-copied twin.
|
|
276
|
+
|
|
277
|
+
```ts
|
|
278
|
+
import { readdirSync, readFileSync, statSync } from 'node:fs'
|
|
279
|
+
import { readFile, stat } from 'node:fs/promises'
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
```ts
|
|
283
|
+
/** Async twin of {@link loadUserTitles}. Same parser, same degradation. Used by
|
|
284
|
+
* computeSessionList so the request path never blocks the loop on this read. */
|
|
285
|
+
export async function loadUserTitlesAsync(accountDir: string | null): Promise<Map<string, string>> {
|
|
286
|
+
if (!accountDir) return new Map()
|
|
287
|
+
let raw: string
|
|
288
|
+
try { raw = await readFile(join(accountDir, 'session-titles.json'), 'utf8') } catch { return new Map() }
|
|
289
|
+
return parseUserTitles(raw)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** Async twin of {@link readSidecarMeta}. Same parser, same degradation. */
|
|
293
|
+
export async function readSidecarMetaAsync(metaPath: string): Promise<SidecarMeta> {
|
|
294
|
+
let raw: string
|
|
295
|
+
try { raw = await readFile(metaPath, 'utf8') } catch { return emptySidecarMeta() }
|
|
296
|
+
return parseSidecarMeta(raw)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** The row loop's read half, exported only so the yield test drives the shipped
|
|
300
|
+
* code. Sequential by design: `Promise.all` here would hold every body in
|
|
301
|
+
* memory at once (133.9 MB measured on maxy 2026-07-26). */
|
|
302
|
+
export async function readAllBodiesForTest(projectsRoot: string): Promise<number> {
|
|
303
|
+
let n = 0
|
|
304
|
+
for (const { path } of enumerateJsonls(projectsRoot)) {
|
|
305
|
+
try { await readFile(path, 'utf8'); await stat(path); n++ } catch { continue }
|
|
306
|
+
}
|
|
307
|
+
return n
|
|
308
|
+
}
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
In `computeSessionList`, change line 572 and the row loop's reads:
|
|
312
|
+
|
|
313
|
+
```ts
|
|
314
|
+
const userTitles = await loadUserTitlesAsync(accountDir)
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
```ts
|
|
318
|
+
let body: string
|
|
319
|
+
let mtimeMs: number
|
|
320
|
+
try {
|
|
321
|
+
body = await readFile(path, 'utf8')
|
|
322
|
+
mtimeMs = (await stat(path)).mtimeMs
|
|
323
|
+
} catch {
|
|
324
|
+
continue
|
|
325
|
+
}
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
```ts
|
|
329
|
+
const { bridgeIds, role, channel: sidecarChannel, senderId, personId, adminUserId, accountId: rowAccountId } = await readSidecarMetaAsync(metaPath)
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
- [ ] **Step 4: Run tests to verify they pass**
|
|
333
|
+
|
|
334
|
+
Run: `cd maxy-code/platform/ui && npx vitest run server/routes/admin/__tests__/sidebar-sessions-async-reads.test.ts`
|
|
335
|
+
Expected: PASS, including the yield test.
|
|
336
|
+
|
|
337
|
+
- [ ] **Step 5: Mutation-check the yield test**
|
|
338
|
+
|
|
339
|
+
Temporarily revert `readAllBodiesForTest` to `readFileSync` / `statSync` and re-run. Expected: the yield test FAILS with `firedDuringWalk` false. Restore the async version. A yield test that passes against synchronous reads is measuring nothing.
|
|
340
|
+
|
|
341
|
+
- [ ] **Step 6: Typecheck and run the full change surface**
|
|
342
|
+
|
|
343
|
+
Run: `cd maxy-code/platform/ui && npx tsc --noEmit -p tsconfig.json`
|
|
344
|
+
Expected: 0 errors.
|
|
345
|
+
|
|
346
|
+
Run: `cd maxy-code/platform/ui && npx vitest run server/routes/admin/__tests__/ server/lib/__tests__/route-cache.test.ts server/__tests__/edge-request-timing.test.ts`
|
|
347
|
+
Expected: PASS. `route-cache-watchers.test.ts` fails only when batched with its siblings and passes 4/4 alone; that is Task 2014 and predates this change.
|
|
348
|
+
|
|
349
|
+
- [ ] **Step 7: Commit**
|
|
350
|
+
|
|
351
|
+
```bash
|
|
352
|
+
git add maxy-code/platform/ui/server/routes/admin/sidebar-sessions.ts maxy-code/platform/ui/server/routes/admin/__tests__/sidebar-sessions-async-reads.test.ts
|
|
353
|
+
git commit -m "perf(2023): read session bodies and sidecars asynchronously so the walk yields the loop"
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
---
|
|
357
|
+
|
|
358
|
+
### Task 3: Correct the task file's own criterion and figures
|
|
359
|
+
|
|
360
|
+
**Files:**
|
|
361
|
+
- Modify: `maxy-code/.tasks/2023-sidebar-sessions-sync-fs-blocks-event-loop.md`
|
|
362
|
+
|
|
363
|
+
**Interfaces:** none.
|
|
364
|
+
|
|
365
|
+
- [ ] **Step 1: Replace the p99 verification criterion**
|
|
366
|
+
|
|
367
|
+
The task says a p99 approaching the request durations confirms and a low p99 refutes. Fourteen consecutive windows measured on maxy read `p99=51` or `52` including one with `max=663`, because one blocked tick out of ~590 samples sits nowhere near the 99th percentile. Rewrite the Observability and Verification sections to turn on `max`, which is the statistic that sees a single sub-second block and which Task 2024 also shipped.
|
|
368
|
+
|
|
369
|
+
- [ ] **Step 2: Replace the 620 ms figure**
|
|
370
|
+
|
|
371
|
+
The `p50 620 ms across 27,280 requests` sample is undatable and mostly predates the Task 1986 route cache, which landed 2026-07-25. State the datable post-cache distribution instead: `n=38 p50=2 p90=603 p99=678 max=1161`, and that the cost is one full walk per 30 s poll because the poll interval equals `BACKSTOP_MS`.
|
|
372
|
+
|
|
373
|
+
- [ ] **Step 3: Record the enumeration deviation**
|
|
374
|
+
|
|
375
|
+
State that the four `readdirSync` calls were measured at 1 ms and deliberately left synchronous, with the six other importers of `enumerateJsonls` as the reason.
|
|
376
|
+
|
|
377
|
+
- [ ] **Step 4: Commit**
|
|
378
|
+
|
|
379
|
+
```bash
|
|
380
|
+
git add maxy-code/.tasks/2023-sidebar-sessions-sync-fs-blocks-event-loop.md
|
|
381
|
+
git commit -m "docs(2023): correct the p99 criterion and the pre-cache 620ms figure"
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
---
|
|
385
|
+
|
|
386
|
+
## Verification (Phase 4, on-device)
|
|
387
|
+
|
|
388
|
+
The unit tests prove the loop yields. They cannot prove the lag falls on real data, so the sprint's claim rests on a device reading taken the same way as the before reading:
|
|
389
|
+
|
|
390
|
+
- Before, already recorded: fourteen windows, `max` 310 to 708 ms in every window containing a `cache=stale-backstop` recompute, `max` 52 to 53 ms in every window without one.
|
|
391
|
+
- After: the same grep across a window of real dashboard traffic. A window containing a `stale-backstop` recompute should no longer show a `max` near the recompute's own `ms=`.
|
|
392
|
+
- Refutation case: if `max` still tracks the recompute duration after the change, the remaining block is the ~215 ms of parsing and the change did not achieve its goal. That is a reportable result, not a failed change.
|
|
393
|
+
|
|
394
|
+
This step needs an installer publish to reach the device and is therefore operator-gated.
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
# Task 1902 — every account-root entry is declared by the component that owns it
|
|
2
|
+
|
|
3
|
+
Design agreed 2026-07-26. Source task: `.tasks/pending/1902-account-owned-entry-declarations-are-incomplete.md`.
|
|
4
|
+
|
|
5
|
+
## The problem this removes
|
|
6
|
+
|
|
7
|
+
`account-schema-owned-dirs.py` unions each plugin's `account-owned-dirs`
|
|
8
|
+
declaration into the account's `SCHEMA.md` `allowed-top-level` block on provision,
|
|
9
|
+
and the standing reconcile
|
|
10
|
+
(`platform/services/claude-session-manager/src/account-dir-schema-reconcile.ts`)
|
|
11
|
+
names every root entry outside that block on the `op=strays` line. Three plugins
|
|
12
|
+
declare anything today: `storage-broker/PLUGIN.md:4` (`pages`),
|
|
13
|
+
`memory/PLUGIN.md:4` (`archive`), and
|
|
14
|
+
`premium-plugins/sitedesk/plugins/sitedesk-job/PLUGIN.md:34` (`quoting`).
|
|
15
|
+
|
|
16
|
+
Two classes of entry are outside that mechanism.
|
|
17
|
+
|
|
18
|
+
Platform-written control-plane files at the account root are kept out of the stray
|
|
19
|
+
count by `PROTECTED_TOP_LEVEL`, a hand-maintained constant in the reconcile that
|
|
20
|
+
has been extended once per casualty (`calendar-availability.json` was Task 1877,
|
|
21
|
+
`agents-disabled.json` was Task 1996). It holds seven such files. The mechanism
|
|
22
|
+
that should own them covers directories only, so no plugin can declare a file, and
|
|
23
|
+
`data-portal.json` was never added to either surface and reports as a stray on
|
|
24
|
+
every run.
|
|
25
|
+
|
|
26
|
+
Directories an agent created at an account root are in no declaration and have no
|
|
27
|
+
writer in the shipped tree. Since Task 1899 removed the move, each costs one
|
|
28
|
+
`op=strays` name per five-minute cycle rather than a broken feature. The cost is a
|
|
29
|
+
permanently noisy signal an operator learns to ignore, which is how the next real
|
|
30
|
+
stray gets missed.
|
|
31
|
+
|
|
32
|
+
## Evidence
|
|
33
|
+
|
|
34
|
+
Grep over `platform/plugins`, `platform/scripts`, `platform/services`,
|
|
35
|
+
`platform/templates` and `premium-plugins` for an account-root write of each
|
|
36
|
+
undeclared name:
|
|
37
|
+
|
|
38
|
+
- `e-sign/` has one writer: `platform/plugins/business-assistant/skills/e-sign/SKILL.md:452`
|
|
39
|
+
(`mkdir -p "${ACCOUNT_DIR}/e-sign/<DOC_REF>"`), plus the persisted base and the
|
|
40
|
+
per-signer stamped copies alongside it.
|
|
41
|
+
- `clients`, `workers`, `specs`, `templates`, `sites-staging` and `repo` have no
|
|
42
|
+
writer. By the source task's own rule an entry with no creator in the tree is
|
|
43
|
+
agent work product, recorded as such rather than declared.
|
|
44
|
+
|
|
45
|
+
Writers for the seven protected files and for `data-portal.json`:
|
|
46
|
+
|
|
47
|
+
| Root file | Writer | Declared on |
|
|
48
|
+
|---|---|---|
|
|
49
|
+
| `calendar-availability.json` | `plugins/scheduling/mcp/src/scripts/publish-availability.ts` | scheduling |
|
|
50
|
+
| `data-portal.json` | `plugins/cloudflare/bin/portal-index-push.mjs` | cloudflare |
|
|
51
|
+
| `wa-channel-bindings.json` | `services/claude-session-manager/src/wa-channel-store.ts` | admin |
|
|
52
|
+
| `telegram-channel-bindings.json` | `services/claude-session-manager/src/telegram-channel-store.ts` | admin |
|
|
53
|
+
| `webchat-channel-bindings.json` | `services/claude-session-manager/src/webchat-channel-store.ts` | admin |
|
|
54
|
+
| `canonical-webchat-session.json` | `ui/server/canonical-webchat-override.ts` | admin |
|
|
55
|
+
| `session-titles.json` | `services/claude-session-manager/src/account-title-stores.ts` | admin |
|
|
56
|
+
| `agents-disabled.json` | `ui/server/routes/admin/agents.ts` | admin |
|
|
57
|
+
|
|
58
|
+
Six of the eight are written by a shared platform service or the UI server, not by
|
|
59
|
+
the plugin they are declared on. The session manager and the UI server carry no
|
|
60
|
+
manifest the resolver reads, so their files are declared on `admin`, and the rule
|
|
61
|
+
is reach rather than naming: **a declaration is only present on a brand whose
|
|
62
|
+
payload carries the declaring plugin.** `brand.json` `plugins.excluded` drops the
|
|
63
|
+
directory outright (`packages/create-maxy-code/scripts/bundle.js`), and
|
|
64
|
+
`realagent-code` and `property-administrators` both exclude `telegram` while the
|
|
65
|
+
session manager still constructs `TelegramChannelStore`
|
|
66
|
+
(`services/claude-session-manager/src/index.ts`). Declaring
|
|
67
|
+
`telegram-channel-bindings.json` on the telegram plugin would therefore leave
|
|
68
|
+
those two brands reporting it as a stray forever, which is the failure this task
|
|
69
|
+
exists to remove. `admin` is excluded by no brand.
|
|
70
|
+
|
|
71
|
+
The three remaining declarations sit on the plugin whose own code writes them
|
|
72
|
+
(`scheduling`, `cloudflare`, `business-assistant`). That is self-consistent: when
|
|
73
|
+
such a plugin is excluded, its writer is excluded with it and the file is never
|
|
74
|
+
created, so the declaration is present exactly when the file can be.
|
|
75
|
+
|
|
76
|
+
`setup-account.sh:69` runs `reconcile_all_accounts_owned_dirs "$ACCOUNTS_DIR"` on
|
|
77
|
+
every install, and that sweep calls `merge_owned_dirs_into_schema` for every
|
|
78
|
+
account carrying an `account.json`. A declaration therefore reaches every existing
|
|
79
|
+
account on the next install, not only accounts provisioned after it lands.
|
|
80
|
+
|
|
81
|
+
## Units
|
|
82
|
+
|
|
83
|
+
### 1. `platform/scripts/lib/account-schema-owned-dirs.py`
|
|
84
|
+
|
|
85
|
+
`_parse_plugin_md` gains a second frontmatter key, `account-owned-files`, parsed
|
|
86
|
+
with the same regex-then-`json.loads` shape as `account-owned-dirs` and keyed
|
|
87
|
+
`file` instead of `dir`:
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
account-owned-files: [{"file": "data-portal.json", "description": "..."}]
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
A declaration that fails to parse is skipped and the remaining declarations still
|
|
94
|
+
merge, matching how the dirs key already behaves (`except Exception: pass`).
|
|
95
|
+
|
|
96
|
+
`resolve` returns one list carrying both kinds, each entry gaining a `kind` field
|
|
97
|
+
of `dir` or `file`, so a single ordered union feeds the merge. Dirs keep their
|
|
98
|
+
existing position ahead of files from the same manifest.
|
|
99
|
+
|
|
100
|
+
`merge` unions declared file names into the same `allowed-top-level` block as
|
|
101
|
+
dirs. The block is already a flat list of names that carries `SCHEMA.md`,
|
|
102
|
+
`account.json` and `AGENTS.md`, and `parseAllowedTopLevel` in the reconcile
|
|
103
|
+
compares it against `readdirSync` entries without distinguishing files from
|
|
104
|
+
directories, so no consumer changes. The prune rule is unchanged: an entry the
|
|
105
|
+
resolver no longer targets is dropped when empty or absent and kept-and-logged
|
|
106
|
+
when it holds content, and `_dir_holds_files` already treats a non-empty regular
|
|
107
|
+
file as content.
|
|
108
|
+
|
|
109
|
+
The plugin-owned descriptive region renders a file without the trailing slash a
|
|
110
|
+
directory gets:
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
- `pages/` — Cloudflare Pages deploy staging for the account's public sites.
|
|
114
|
+
- `data-portal.json` — the account's data-portal index, written by portal-index-push.
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
`reconcile` prints `ownedFile=<name> present=<true|false>`, the file twin of the
|
|
118
|
+
existing `ownedDir=` line, so a declaration that has not reached an account is
|
|
119
|
+
visible per name.
|
|
120
|
+
|
|
121
|
+
### 2. Declarations
|
|
122
|
+
|
|
123
|
+
One frontmatter line added per plugin, in the shape the resolver parses:
|
|
124
|
+
|
|
125
|
+
- `plugins/business-assistant/PLUGIN.md` — `account-owned-dirs` gains `e-sign`.
|
|
126
|
+
- `plugins/cloudflare/PLUGIN.md` — `account-owned-files` gains `data-portal.json`.
|
|
127
|
+
- `plugins/scheduling/PLUGIN.md` — `calendar-availability.json`.
|
|
128
|
+
- `plugins/admin/PLUGIN.md` — `wa-channel-bindings.json`,
|
|
129
|
+
`telegram-channel-bindings.json`, `webchat-channel-bindings.json`,
|
|
130
|
+
`canonical-webchat-session.json`, `session-titles.json`, `agents-disabled.json`.
|
|
131
|
+
|
|
132
|
+
The resolver reads every `$PROJECT_DIR/plugins/*/PLUGIN.md` present in the
|
|
133
|
+
installed tree, with only premium bundles gated by `shipsPremiumBundles`. A brand
|
|
134
|
+
that excludes a plugin has no manifest to read, which is why the six
|
|
135
|
+
service-written files sit on `admin`: every brand then allows all nine names,
|
|
136
|
+
which is what `PROTECTED_TOP_LEVEL` did for seven of them.
|
|
137
|
+
|
|
138
|
+
### 3. `platform/services/claude-session-manager/src/account-dir-schema-reconcile.ts`
|
|
139
|
+
|
|
140
|
+
`PROTECTED_TOP_LEVEL` drops the seven file entries and keeps `.git`, `.claude`,
|
|
141
|
+
`.quarantine`, `SCHEMA.md`, `account.json` and `AGENTS.md`. Its comment is
|
|
142
|
+
rewritten to state the new division: the constant holds the account's own
|
|
143
|
+
machinery, which exists whatever the schema says, and every feature file is held
|
|
144
|
+
by the declaration that owns it.
|
|
145
|
+
|
|
146
|
+
The behavioural consequence, stated rather than hidden: those seven files are now
|
|
147
|
+
kept out of the stray count by the account's `SCHEMA.md`, not by a constant. An
|
|
148
|
+
account whose schema was never re-merged reports them. The sweep in
|
|
149
|
+
`setup-account.sh` closes that window on the install that ships this change, and
|
|
150
|
+
the `ownedFile=` reconcile line names any account it has not reached.
|
|
151
|
+
|
|
152
|
+
### 4. Tests
|
|
153
|
+
|
|
154
|
+
`platform/scripts/__tests__/account-schema-owned-dirs.test.sh` gains, on a
|
|
155
|
+
synthetic tree in the style of the existing shape-b case:
|
|
156
|
+
|
|
157
|
+
- a plugin declaring `account-owned-files: [{"file": "x.json", ...}]` puts `x.json`
|
|
158
|
+
in the merged `allowed-top-level` block, describes it in the plugin-owned region
|
|
159
|
+
without a trailing slash, and reports `ownedFile=x.json present=true` from
|
|
160
|
+
`reconcile`;
|
|
161
|
+
- a plugin whose `account-owned-files` value is malformed JSON is skipped while a
|
|
162
|
+
sibling plugin's valid declaration still merges;
|
|
163
|
+
- the merge stays byte-identical on a second run with a file declaration present.
|
|
164
|
+
|
|
165
|
+
**Mutation check.** Drop the files branch from the resolver and the new cases must
|
|
166
|
+
fail. Revert.
|
|
167
|
+
|
|
168
|
+
`platform/services/claude-session-manager/src/__tests__/account-dir-schema-reconcile.test.ts`
|
|
169
|
+
carries two tests that prove the code belt: `never counts platform-written
|
|
170
|
+
control-plane root files absent from the allowed block` and `does not count the
|
|
171
|
+
disabled store itself as a stray`. Both seed an account whose `SCHEMA.md` lists no
|
|
172
|
+
such file, which after this change is a stale account rather than a current one.
|
|
173
|
+
They are rewritten to seed the schema a reprovisioned account carries, listing the
|
|
174
|
+
declared file names, and then assert the same outcome: the control-plane files are
|
|
175
|
+
absent from `strayNames` and a genuine operator stray alongside them is still
|
|
176
|
+
counted and named. That is the surface the reconcile reads, so the rewritten tests
|
|
177
|
+
prove the delivered behaviour rather than the retired constant.
|
|
178
|
+
|
|
179
|
+
### 5. The region parsers and the doc
|
|
180
|
+
|
|
181
|
+
Three readers parse the plugin-owned region and each anchors on the trailing
|
|
182
|
+
slash inside the backticks: `platform/lib/account-schema-regions/src/index.ts:65`
|
|
183
|
+
(shared by the `/data` page grouping and the reconcile's parity audit) and
|
|
184
|
+
`platform/plugins/cloudflare/bin/schema-exposed-dirs.mjs:42-43` (the public
|
|
185
|
+
data-portal exposure set, which keeps its own copy under a parity test). A file
|
|
186
|
+
line therefore yields no bucket in any of them, which is the required behaviour
|
|
187
|
+
and needs no code change. It is pinned by a case in
|
|
188
|
+
`platform/ui/server/lib/__tests__/account-schema-regions.test.ts`, because a
|
|
189
|
+
future edit relaxing that regex would put a control-plane file in front of a
|
|
190
|
+
client.
|
|
191
|
+
|
|
192
|
+
`.docs/data-portal-folder-index.md` is the authoritative description of how the
|
|
193
|
+
allowed set is built and currently describes plugin-owned as directories. It
|
|
194
|
+
gains the second kind and the trailing-slash rule.
|
|
195
|
+
|
|
196
|
+
## Scope boundaries
|
|
197
|
+
|
|
198
|
+
### In scope
|
|
199
|
+
|
|
200
|
+
Units 1 to 5.
|
|
201
|
+
|
|
202
|
+
### Out of scope
|
|
203
|
+
|
|
204
|
+
- Reprovisioning the laptop's accounts, and the two device checks that depend on
|
|
205
|
+
it (`op=strays` naming only recorded work product, and the negative check that
|
|
206
|
+
removing one declaration makes its entry reappear). Both need this change
|
|
207
|
+
published and installed, and publish awaits an explicit operator command. Task
|
|
208
|
+
`2027-reprovision-laptop-accounts-after-1902-declarations.md`.
|
|
209
|
+
- Declaring `clients`, `workers`, `specs`, `templates`, `sites-staging` or `repo`.
|
|
210
|
+
No shipped component writes them, so declaring them would make the allowed list
|
|
211
|
+
a record of what happens to exist rather than of what is owned. They continue to
|
|
212
|
+
appear in `op=strays`, which is the correct report for debris.
|
|
213
|
+
- Relocating agent work product into schema buckets. Task 1894 covers the glsmith
|
|
214
|
+
case.
|
|
215
|
+
- Teaching `fs-schema-guard.sh` to tell a declared file from an operator bucket.
|
|
216
|
+
The guard reads the same `allowed-top-level` fence as an allow-list for
|
|
217
|
+
`Write`/`Edit`/`NotebookEdit`, so declaring these eight files makes them
|
|
218
|
+
agent-writable where they were blocked before. Measured: the same envelope
|
|
219
|
+
returns exit 2 against the pre-merge schema and exit 0 against the merged one.
|
|
220
|
+
The fence already permits `account.json`, `SCHEMA.md`, `secrets/` and
|
|
221
|
+
`.claude/`, so it is a layout guard rather than a trust boundary, and this
|
|
222
|
+
widens it rather than breaches it. Task
|
|
223
|
+
`2028-fs-schema-guard-cannot-tell-a-declared-file-from-an-operator-bucket.md`.
|
|
224
|
+
- The reconcile's report-only posture. Task 1899 settled it.
|
|
225
|
+
|
|
226
|
+
## Observability
|
|
227
|
+
|
|
228
|
+
The `op=strays` line from Task 1899 is the instrument. A declared entry never
|
|
229
|
+
appears in it, so anything that does is either debris or an undeclared owner, and
|
|
230
|
+
the two are separated by asking what created it. The `ownedDir=` and `ownedFile=`
|
|
231
|
+
lines from `reconcile` are the second instrument: they name, per account, a
|
|
232
|
+
declaration that has not yet reached that account's `SCHEMA.md`.
|