@maudecode/talaria-web 1.20.0 → 1.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/_release.json +8 -8
- package/dist/api/automation-router.d.ts +313 -8
- package/dist/api/automation-router.js +21 -1
- package/dist/api/automation-router.js.map +1 -1
- package/dist/api/chat-router.d.ts +412 -15
- package/dist/api/chat-router.js +22 -170
- package/dist/api/chat-router.js.map +1 -1
- package/dist/api/git-router.d.ts +50 -0
- package/dist/api/git-router.js +17 -4
- package/dist/api/git-router.js.map +1 -1
- package/dist/api/router.d.ts +1 -1
- package/dist/api/sessions-router.d.ts +1138 -29
- package/dist/api/sessions-router.js +58 -41
- package/dist/api/sessions-router.js.map +1 -1
- package/dist/api/sse-routes.js +4 -2
- package/dist/api/sse-routes.js.map +1 -1
- package/dist/api/tools-router.d.ts +481 -29
- package/dist/api/tools-router.js +34 -9
- package/dist/api/tools-router.js.map +1 -1
- package/dist/http/context.d.ts +2 -0
- package/dist/http/context.js.map +1 -1
- package/dist/redact.d.ts +2 -0
- package/dist/redact.js +17 -2
- package/dist/redact.js.map +1 -1
- package/dist/runtime.js +36 -8
- package/dist/runtime.js.map +1 -1
- package/dist/server.js +1 -0
- package/dist/server.js.map +1 -1
- package/dist/sessions/admission.d.ts +21 -0
- package/dist/sessions/admission.js +176 -0
- package/dist/sessions/admission.js.map +1 -0
- package/dist/sessions/anchor.d.ts +2 -0
- package/dist/sessions/anchor.js +4 -4
- package/dist/sessions/anchor.js.map +1 -1
- package/dist/sessions/background-tasks.d.ts +19 -1
- package/dist/sessions/background-tasks.js +36 -6
- package/dist/sessions/background-tasks.js.map +1 -1
- package/dist/sessions/events.d.ts +3 -0
- package/dist/sessions/events.js +4 -0
- package/dist/sessions/events.js.map +1 -1
- package/dist/sessions/gateway-sessions.d.ts +50 -2
- package/dist/sessions/gateway-sessions.js +144 -11
- package/dist/sessions/gateway-sessions.js.map +1 -1
- package/dist/sessions/merge.d.ts +1 -0
- package/dist/sessions/merge.js +7 -2
- package/dist/sessions/merge.js.map +1 -1
- package/dist/sessions/prefix-index.d.ts +8 -0
- package/dist/sessions/prefix-index.js +14 -3
- package/dist/sessions/prefix-index.js.map +1 -1
- package/dist/sessions/prompt-queues.d.ts +36 -0
- package/dist/sessions/prompt-queues.js +318 -0
- package/dist/sessions/prompt-queues.js.map +1 -0
- package/dist/sessions/relay.d.ts +12 -1
- package/dist/sessions/relay.js +75 -17
- package/dist/sessions/relay.js.map +1 -1
- package/dist/sessions/service.d.ts +26 -1
- package/dist/sessions/service.js +200 -6
- package/dist/sessions/service.js.map +1 -1
- package/dist/sessions/session.js +1 -0
- package/dist/sessions/session.js.map +1 -1
- package/dist/sessions/storage/json-documents.js +1 -0
- package/dist/sessions/storage/json-documents.js.map +1 -1
- package/dist/sessions/store.d.ts +1 -0
- package/dist/sessions/store.js +1 -0
- package/dist/sessions/store.js.map +1 -1
- package/dist/sessions/streams.d.ts +2 -0
- package/dist/sessions/streams.js.map +1 -1
- package/dist/sessions/tool-display.d.ts +8 -1
- package/dist/sessions/tool-display.js +39 -4
- package/dist/sessions/tool-display.js.map +1 -1
- package/dist/sessions/turn.d.ts +4 -1
- package/dist/sessions/turn.js +20 -13
- package/dist/sessions/turn.js.map +1 -1
- package/dist/tools/gateway-restart.d.ts +1 -1
- package/dist/tools/kanban.d.ts +10 -1
- package/dist/tools/kanban.js +73 -1
- package/dist/tools/kanban.js.map +1 -1
- package/dist/tools/mcp.d.ts +3 -1
- package/dist/tools/mcp.js +73 -0
- package/dist/tools/mcp.js.map +1 -1
- package/dist/tools/memory.d.ts +28 -0
- package/dist/tools/memory.js +120 -14
- package/dist/tools/memory.js.map +1 -1
- package/dist/tools/update-notifications.d.ts +12 -10
- package/dist/tools/update-notifications.js +124 -14
- package/dist/tools/update-notifications.js.map +1 -1
- package/dist/tools/updates.d.ts +22 -3
- package/dist/tools/updates.js +118 -20
- package/dist/tools/updates.js.map +1 -1
- package/dist/workspace/managed-buffers.d.ts +58 -0
- package/dist/workspace/managed-buffers.js +76 -0
- package/dist/workspace/managed-buffers.js.map +1 -0
- package/dist/workspace/rollback.js +1 -1
- package/dist/workspace/rollback.js.map +1 -1
- package/package.json +2 -2
- package/sidecar/agent_dependency.json +4 -4
- package/sidecar/agent_support.json +20 -8
- package/sidecar/talaria_sidecar/__init__.py +1 -1
- package/sidecar/talaria_sidecar/agent_mutation.py +87 -9
- package/sidecar/talaria_sidecar/agent_services.py +83 -23
- package/sidecar/talaria_sidecar/methods/__init__.py +2 -2
- package/sidecar/talaria_sidecar/methods/dashboard_plugins.py +4 -4
- package/sidecar/talaria_sidecar/methods/gateway_sessions.py +109 -19
- package/sidecar/talaria_sidecar/methods/kanban.py +48 -0
- package/sidecar/talaria_sidecar/methods/managed_buffers.py +110 -0
- package/sidecar/talaria_sidecar/methods/mcp.py +55 -0
- package/sidecar/talaria_sidecar/methods/outbound_drafts.py +165 -0
- package/static/dist/FILES.txt +127 -128
- package/static/dist/assets/AppShell-c5989995.js +8 -0
- package/static/dist/assets/Button-nuaikybd.js +1 -0
- package/static/dist/assets/ChatPage-bfg4khph.js +6 -0
- package/static/dist/assets/ConnectTargets-k5wo9yo8.js +1 -0
- package/static/dist/assets/{Dialog-hta5utr3.js → Dialog-o8vo2aws.js} +1 -1
- package/static/dist/assets/{ExtensionRoute-zrhjn0wm.js → ExtensionRoute-l1yjrso4.js} +1 -1
- package/static/dist/assets/Field-j7o8cfps.js +1 -0
- package/static/dist/assets/HubRoute-efnwe4xz.js +2 -0
- package/static/dist/assets/Markdown-clqf5m22.js +2 -0
- package/static/dist/assets/{MarkdownRenderer-fbswsgld.js → MarkdownRenderer-ccgqrza3.js} +18 -18
- package/static/dist/assets/OAuthSignIn-h7bjaqp6.js +1 -0
- package/static/dist/assets/{ResourceSwitch-odi6dy2o.js → ResourceSwitch-oai2zqaj.js} +1 -1
- package/static/dist/assets/RightPanel-ho161a45.js +1 -0
- package/static/dist/assets/{Select-kr07928w.js → Select-d1g5a8bm.js} +1 -1
- package/static/dist/assets/SessionListPanel-i7kaokhi.js +1 -0
- package/static/dist/assets/SettingsLayout-lnecm87w.js +1 -0
- package/static/dist/assets/{States-icab19yg.js → States-easntmry.js} +1 -1
- package/static/dist/assets/TerminalPanel-lxhwd4jl.js +16 -0
- package/static/dist/assets/Toaster-l4sciy8t.js +1 -0
- package/static/dist/assets/_app-mqexqbg8.js +1 -0
- package/static/dist/assets/{_app.ext._extensionId-na62087t.js → _app.ext._extensionId-n797jp72.js} +1 -1
- package/static/dist/assets/{_app.index-jypgdqoi.js → _app.index-ne43mdw2.js} +1 -1
- package/static/dist/assets/{_app.insights-kabejdtm.js → _app.insights-fqonovb4.js} +1 -1
- package/static/dist/assets/{_app.kanban-m19kfcmx.js → _app.kanban-kysliv19.js} +1 -1
- package/static/dist/assets/{_app.logs-iw4m96sz.js → _app.logs-febut82r.js} +1 -1
- package/static/dist/assets/{_app.memory-jq5frwcg.js → _app.memory-ha86oj1c.js} +1 -1
- package/static/dist/assets/{_app.profiles-kwa3m7rp.js → _app.profiles-cnu3gxxq.js} +1 -1
- package/static/dist/assets/{_app.session._sessionId-o5lyeipj.js → _app.session._sessionId-ifzjpcgn.js} +1 -1
- package/static/dist/assets/{_app.settings-ckl8hdll.js → _app.settings-nmakdiaf.js} +1 -1
- package/static/dist/assets/_app.settings._section-djglnjmi.js +3 -0
- package/static/dist/assets/{_app.skills-n016rihd.js → _app.skills-f3wad8yo.js} +1 -1
- package/static/dist/assets/_app.tasks-l10py1c0.js +1 -0
- package/static/dist/assets/{_app.todos-btew5v2s.js → _app.todos-bml1m3r1.js} +1 -1
- package/static/dist/assets/{_app.workspaces-lnfk95ch.js → _app.workspaces-f3d066lx.js} +1 -1
- package/static/dist/assets/{abnfDiagram-VCTEODGH-dttheegb.js → abnfDiagram-VCTEODGH-i53jc4yp.js} +1 -1
- package/static/dist/assets/{architecture-7GRP2DOG-ic435919.js → architecture-7GRP2DOG-e3bv5m6f.js} +1 -1
- package/static/dist/assets/{architectureDiagram-5GKGNRK7-8r3glgiq.js → architectureDiagram-5GKGNRK7-pakekxu2.js} +1 -1
- package/static/dist/assets/{archive-il7c6053.js → archive-ihqi7z2x.js} +1 -1
- package/static/dist/assets/{blockDiagram-I7D4REHJ-jenyos6c.js → blockDiagram-I7D4REHJ-bc6bo1n6.js} +1 -1
- package/static/dist/assets/{c4Diagram-7LVT6UL2-nzlx7yc4.js → c4Diagram-7LVT6UL2-btvwk2qx.js} +1 -1
- package/static/dist/assets/channel-jn4f8443.js +1 -0
- package/static/dist/assets/{chunk-4HAMMTFA-g0cru63b.js → chunk-4HAMMTFA-dlwab11c.js} +1 -1
- package/static/dist/assets/{chunk-75Z2AOVW-7rkswkjw.js → chunk-75Z2AOVW-dp5gh7i8.js} +1 -1
- package/static/dist/assets/{chunk-DU6HZSFF-fn73jzdo.js → chunk-DU6HZSFF-bi06zl1c.js} +1 -1
- package/static/dist/assets/{chunk-F27PBJKO-oey8e72m.js → chunk-F27PBJKO-kvv1rnlh.js} +1 -1
- package/static/dist/assets/{chunk-GMAD6QVW-l2a32wiy.js → chunk-GMAD6QVW-ji0rx9by.js} +1 -1
- package/static/dist/assets/{chunk-GVQU2GXP-o9dwzh8h.js → chunk-GVQU2GXP-nmz0j82p.js} +1 -1
- package/static/dist/assets/{chunk-IMKFNOWR-eblwj20b.js → chunk-IMKFNOWR-kmi1qx88.js} +1 -1
- package/static/dist/assets/{chunk-L3NEJ4N5-o2mufspb.js → chunk-L3NEJ4N5-xhb7mebj.js} +1 -1
- package/static/dist/assets/{chunk-OSK3NFVY-ijp9pw29.js → chunk-OSK3NFVY-5a9sgscq.js} +1 -1
- package/static/dist/assets/{chunk-P2QGCYS3-hs9hqwl4.js → chunk-P2QGCYS3-dfezp4vu.js} +1 -1
- package/static/dist/assets/{chunk-POPQ4Y6H-bp9rsqez.js → chunk-POPQ4Y6H-gd2vq3c0.js} +1 -1
- package/static/dist/assets/{chunk-PWAF6VOD-brkprov6.js → chunk-PWAF6VOD-wvw2nksf.js} +1 -1
- package/static/dist/assets/{chunk-SHT3W25Y-gmpv0pxz.js → chunk-SHT3W25Y-f58q97px.js} +1 -1
- package/static/dist/assets/{chunk-SVP7TREG-oldm08fu.js → chunk-SVP7TREG-abgwpyz9.js} +1 -1
- package/static/dist/assets/{chunk-TICWLB2K-ooqilfv7.js → chunk-TICWLB2K-h4sbs8ds.js} +1 -1
- package/static/dist/assets/classDiagram-ZZMXUADV-dzrw66se.js +1 -0
- package/static/dist/assets/classDiagram-v2-VYDZK3BY-dzrw66se.js +1 -0
- package/static/dist/assets/completionAudio-ce86msbu.js +1 -0
- package/static/dist/assets/{copy-jo006coq.js → copy-m9h56qa8.js} +1 -1
- package/static/dist/assets/{cynefin-OW5HDTMX-d47op7qs.js → cynefin-OW5HDTMX-g081vffs.js} +1 -1
- package/static/dist/assets/{cynefinDiagram-5FMLGOSQ-doa7594y.js → cynefinDiagram-5FMLGOSQ-odxvjesk.js} +1 -1
- package/static/dist/assets/{dagre-GXQ25YYZ-os768dp4.js → dagre-GXQ25YYZ-cpdwzjs4.js} +1 -1
- package/static/dist/assets/{diagram-S7CK7UJ4-fyi81nxd.js → diagram-S7CK7UJ4-clv5mxvu.js} +1 -1
- package/static/dist/assets/{diagram-UQ7AKVKN-j3exggaq.js → diagram-UQ7AKVKN-m2ia1mbf.js} +1 -1
- package/static/dist/assets/{diagram-VSXAHHWV-omo1en2l.js → diagram-VSXAHHWV-ea6zv636.js} +1 -1
- package/static/dist/assets/{diagram-VX7I27RA-oro2cyu0.js → diagram-VX7I27RA-oqlef685.js} +1 -1
- package/static/dist/assets/{diagram-Z3DM3KII-byt9vdb7.js → diagram-Z3DM3KII-kjubcbwu.js} +1 -1
- package/static/dist/assets/{dist-etpoyscq.js → dist-carq1azj.js} +4 -4
- package/static/dist/assets/{ebnfDiagram-PWID7BFC-ee3haugh.js → ebnfDiagram-PWID7BFC-fg0zri3e.js} +1 -1
- package/static/dist/assets/{erDiagram-RLTQ6QDP-d1e93yff.js → erDiagram-RLTQ6QDP-eb7pmk3x.js} +1 -1
- package/static/dist/assets/{eventmodeling-NTZA5JFV-i1ihb40c.js → eventmodeling-NTZA5JFV-gvi1fxhn.js} +1 -1
- package/static/dist/assets/flowDiagram-HODETNUW-abekab15.js +1 -0
- package/static/dist/assets/folder-plus-ksaons88.js +1 -0
- package/static/dist/assets/{ganttDiagram-EL5Y4UJY-m7g0jud4.js → ganttDiagram-EL5Y4UJY-jmisowrc.js} +1 -1
- package/static/dist/assets/{gitGraph-4MIJSDKK-hzdj07yy.js → gitGraph-4MIJSDKK-m7kt3u6e.js} +1 -1
- package/static/dist/assets/{gitGraphDiagram-WWUBYQGX-dm9agbna.js → gitGraphDiagram-WWUBYQGX-nq7vfzh2.js} +1 -1
- package/static/dist/assets/{highlighted-body-KPVGNVTW-mmgz2czu.js → highlighted-body-KPVGNVTW-gcu017ay.js} +1 -1
- package/static/dist/assets/{host-fr6lni1p.js → host-ipp4e84c.js} +1 -1
- package/static/dist/assets/{index-73649w7o.js → index-d6vbxlra.js} +7 -4
- package/static/dist/assets/index-ey6qo0zx.css +2 -0
- package/static/dist/assets/{info-A6RAGUB7-lll1dqmm.js → info-A6RAGUB7-idazwfqe.js} +1 -1
- package/static/dist/assets/{infoDiagram-27XIBGKW-k7esc639.js → infoDiagram-27XIBGKW-bb9jh8gl.js} +1 -1
- package/static/dist/assets/{ishikawaDiagram-5VMMS53U-lgr6k9wo.js → ishikawaDiagram-5VMMS53U-nqc132rv.js} +1 -1
- package/static/dist/assets/{journeyDiagram-3NMN7TZE-ndmbxuqk.js → journeyDiagram-3NMN7TZE-fpvnhy44.js} +1 -1
- package/static/dist/assets/{kanban-definition-UXKFOSKX-mjxym0jp.js → kanban-definition-UXKFOSKX-ndzqm6fu.js} +1 -1
- package/static/dist/assets/{lazyRouteComponent-cwb6l7xh.js → lazyRouteComponent-javud235.js} +1 -1
- package/static/dist/assets/{line-mtr5k6il.js → line-g625e7cj.js} +1 -1
- package/static/dist/assets/{link-iadw4ghw.js → link-gfdaho08.js} +1 -1
- package/static/dist/assets/{login-lkhtzfcl.js → login-iv6yq4xf.js} +1 -1
- package/static/dist/assets/mermaid-HWGCJPDP-cwj5cl6k.js +1 -0
- package/static/dist/assets/{mermaid-parser.core-k91vdnvi.js → mermaid-parser.core-plzkuzov.js} +3 -3
- package/static/dist/assets/{mindmap-definition-YA3MSWOX-ehztfea4.js → mindmap-definition-YA3MSWOX-ojrgs569.js} +1 -1
- package/static/dist/assets/onboarding-ff4emacu.js +1 -0
- package/static/dist/assets/{packet-AYTQ26CC-ok2xrblz.js → packet-AYTQ26CC-gyaf9s8h.js} +1 -1
- package/static/dist/assets/{pegDiagram-XKGWAZYB-ec3rpj0m.js → pegDiagram-XKGWAZYB-bf6xj8mq.js} +1 -1
- package/static/dist/assets/{pie-WAS4IAKB-iwkir23u.js → pie-WAS4IAKB-f5ymzelo.js} +1 -1
- package/static/dist/assets/{pieDiagram-E7YTZNPT-fcpqpekh.js → pieDiagram-E7YTZNPT-d0qjl3zi.js} +1 -1
- package/static/dist/assets/{quadrantDiagram-AXDQQJYC-lqsync43.js → quadrantDiagram-AXDQQJYC-oqy1mras.js} +1 -1
- package/static/dist/assets/queryKeys-x8fjxhut.js +9 -0
- package/static/dist/assets/{radar-RG4KPBEZ-hje4jbk6.js → radar-RG4KPBEZ-b6jcjvda.js} +1 -1
- package/static/dist/assets/{railroad-74A4TZTK-gll9n6qi.js → railroad-74A4TZTK-e3a6lk5k.js} +1 -1
- package/static/dist/assets/{railroad-abnf-HS5TGJTU-o6h6lpn4.js → railroad-abnf-HS5TGJTU-ff5hocrx.js} +1 -1
- package/static/dist/assets/{railroad-ebnf-LZEXJU2U-juaefj6o.js → railroad-ebnf-LZEXJU2U-exkhyw4n.js} +1 -1
- package/static/dist/assets/{railroad-peg-WCYAUIDC-cgfhvpah.js → railroad-peg-WCYAUIDC-jefk9b7c.js} +1 -1
- package/static/dist/assets/{railroadDiagram-O6MQD6OU-lp3w7awf.js → railroadDiagram-O6MQD6OU-e4amks1i.js} +1 -1
- package/static/dist/assets/{refresh-cw-iccm7a4e.js → refresh-cw-lfp4u1oh.js} +1 -1
- package/static/dist/assets/{requirementDiagram-BXWQKSXE-ha8eibcf.js → requirementDiagram-BXWQKSXE-c77k1e2y.js} +1 -1
- package/static/dist/assets/{sankeyDiagram-P5KCCOFB-oxf31pr9.js → sankeyDiagram-P5KCCOFB-oa1cc52s.js} +1 -1
- package/static/dist/assets/{sequenceDiagram-WJ2MYXX4-jiq8lhyo.js → sequenceDiagram-WJ2MYXX4-j20gqt22.js} +1 -1
- package/static/dist/assets/{share._token-o76ki0ly.js → share._token-kmfoflw6.js} +2 -2
- package/static/dist/assets/{sliders-horizontal-ddcs88av.js → sliders-horizontal-g5jkzd9k.js} +1 -1
- package/static/dist/assets/{stateDiagram-D77RDMKH-f3fslnon.js → stateDiagram-D77RDMKH-co2ghkdp.js} +1 -1
- package/static/dist/assets/stateDiagram-v2-MP3YSRHH-faw1opgh.js +1 -0
- package/static/dist/assets/{swimlanes-42K2YHIH-k1hmk7jf.js → swimlanes-42K2YHIH-m30cktqe.js} +1 -1
- package/static/dist/assets/swimlanesDiagram-VR7AAH4N-hfil433g.js +8 -0
- package/static/dist/assets/{timeline-definition-24CTP7MA-n3ncfy9r.js → timeline-definition-24CTP7MA-liuep6fl.js} +1 -1
- package/static/dist/assets/toast-zmb35u3r.js +56 -0
- package/static/dist/assets/{treeView-Q6P3EWNA-l3y2pejt.js → treeView-Q6P3EWNA-np5d2jv2.js} +1 -1
- package/static/dist/assets/{treemap-WGGIJYW6-jown9axh.js → treemap-WGGIJYW6-cmpltxh1.js} +1 -1
- package/static/dist/assets/{useForm-o4uu74mx.js → useForm-f1udzsjf.js} +1 -1
- package/static/dist/assets/{useParams-ftsveu84.js → useParams-ierevvts.js} +1 -1
- package/static/dist/assets/{useSearch-crqueqga.js → useSearch-i7nr9ypl.js} +1 -1
- package/static/dist/assets/{vennDiagram-4TSXK5OY-dh59uq1c.js → vennDiagram-4TSXK5OY-b2g42qrg.js} +1 -1
- package/static/dist/assets/{wardley-WFR3VGLG-e6d74b0j.js → wardley-WFR3VGLG-lb0eqbff.js} +1 -1
- package/static/dist/assets/{wardleyDiagram-VM6X3IG4-hnofh78f.js → wardleyDiagram-VM6X3IG4-ermwaqha.js} +1 -1
- package/static/dist/assets/{xychartDiagram-S5SC5T6Z-bpymjelb.js → xychartDiagram-S5SC5T6Z-dll0jldk.js} +1 -1
- package/static/dist/index.html +1 -1
- package/static/dist/sw.js +1 -1
- package/static/dist/assets/AppShell-3isgvoro.js +0 -8
- package/static/dist/assets/Button-f4n6dapf.js +0 -1
- package/static/dist/assets/ChatPage-k7b6xfax.js +0 -4
- package/static/dist/assets/ConnectTargets-juqvt5lm.js +0 -1
- package/static/dist/assets/Field-b6ez48oz.js +0 -1
- package/static/dist/assets/HubRoute-iwvso2tz.js +0 -2
- package/static/dist/assets/Markdown-jdeth7zq.js +0 -2
- package/static/dist/assets/OAuthSignIn-bpd8hom3.js +0 -1
- package/static/dist/assets/RightPanel-eu6enlo6.js +0 -1
- package/static/dist/assets/SessionListPanel-etb2i54l.js +0 -1
- package/static/dist/assets/SettingsLayout-eu3xxp23.js +0 -1
- package/static/dist/assets/TerminalPanel-hngmsr8b.js +0 -16
- package/static/dist/assets/Toaster-nb2uzjw1.js +0 -1
- package/static/dist/assets/_app-ga1moszl.js +0 -1
- package/static/dist/assets/_app.settings._section-dtdjk98a.js +0 -3
- package/static/dist/assets/_app.tasks-n8zuqsqe.js +0 -1
- package/static/dist/assets/cancel-bsnfo36u.js +0 -1
- package/static/dist/assets/channel-dsbtk01y.js +0 -1
- package/static/dist/assets/classDiagram-ZZMXUADV-fvgcv24g.js +0 -1
- package/static/dist/assets/classDiagram-v2-VYDZK3BY-fvgcv24g.js +0 -1
- package/static/dist/assets/flowDiagram-HODETNUW-jahfpiod.js +0 -1
- package/static/dist/assets/folder-plus-ivoglq98.js +0 -1
- package/static/dist/assets/index-lhf1fsdf.css +0 -2
- package/static/dist/assets/mermaid-HWGCJPDP-jx0q5dm2.js +0 -1
- package/static/dist/assets/onboarding-f67z0a2r.js +0 -1
- package/static/dist/assets/queryKeys-ed4n2egc.js +0 -9
- package/static/dist/assets/refresh-i9ya9dvl.js +0 -1
- package/static/dist/assets/stateDiagram-v2-MP3YSRHH-hlt6yphw.js +0 -1
- package/static/dist/assets/swimlanesDiagram-VR7AAH4N-hd5t9tnt.js +0 -8
- package/static/dist/assets/toast-liparz2j.js +0 -56
|
@@ -164,12 +164,15 @@ def snapshot(entry, *, records=None, expected=None):
|
|
|
164
164
|
from gateway.status import get_process_start_time, _pid_exists
|
|
165
165
|
service, pid = entry.get('service_pid', entry['pid']), entry['pid']
|
|
166
166
|
anchors = {service, pid}
|
|
167
|
-
observed =
|
|
167
|
+
observed = {**entry.get('departed_processes', {}), **(expected or {})}
|
|
168
168
|
seen_parents = {int(p): proof['parent'] for p, proof in observed.items() if 'parent' in proof}
|
|
169
|
-
departed = set()
|
|
169
|
+
departed = set(map(int, entry.get('departed_processes', {})))
|
|
170
170
|
|
|
171
171
|
def exited(p):
|
|
172
|
-
|
|
172
|
+
live = _pid_exists(p)
|
|
173
|
+
if live is None:
|
|
174
|
+
raise RuntimeError(f'{_name(entry)} process {p} liveness is unreadable; source was not changed.')
|
|
175
|
+
if live is False:
|
|
173
176
|
if p in anchors:
|
|
174
177
|
raise RuntimeError(f'{_name(entry)} anchor process {p} disappeared during discovery; source was not changed.')
|
|
175
178
|
departed.add(p)
|
|
@@ -179,7 +182,7 @@ def snapshot(entry, *, records=None, expected=None):
|
|
|
179
182
|
def inventory():
|
|
180
183
|
parents = _process_table()
|
|
181
184
|
if service not in _ancestors(pid, parents):
|
|
182
|
-
raise RuntimeError(f'{_name(entry)} does not own backend/gateway PID {pid}.')
|
|
185
|
+
raise RuntimeError(f'{_name(entry)} identity/ancestry changed: does not own backend/gateway PID {pid}.')
|
|
183
186
|
if service in _ancestors(os.getpid(), parents):
|
|
184
187
|
raise RuntimeError(f'{_name(entry)} owns the updater or an ancestor; source was not changed.')
|
|
185
188
|
for p, parent in seen_parents.items():
|
|
@@ -188,6 +191,10 @@ def snapshot(entry, *, records=None, expected=None):
|
|
|
188
191
|
continue
|
|
189
192
|
raise RuntimeError(f'{_name(entry)} process {p} identity/ancestry changed during discovery; source was not changed.')
|
|
190
193
|
descendants = _descendants(service, parents)
|
|
194
|
+
# Successor retries cannot discard a live writer from a previously observed tree.
|
|
195
|
+
for p in seen_parents.keys() - descendants - departed:
|
|
196
|
+
if not exited(p):
|
|
197
|
+
raise RuntimeError(f'{_name(entry)} process {p} remains live outside the attested tree; recovery remains owed.')
|
|
191
198
|
seen_parents.update({p: parents[p] for p in descendants})
|
|
192
199
|
# Zombies may remain in ps. Recheck departed PIDs at every inventory so
|
|
193
200
|
# neither a recycled PID nor unknown liveness can be silently excluded.
|
|
@@ -205,6 +212,8 @@ def snapshot(entry, *, records=None, expected=None):
|
|
|
205
212
|
if exited(p):
|
|
206
213
|
return None
|
|
207
214
|
raise RuntimeError(f'{_name(entry)} process {p} birth identity is live or unreadable; source was not changed.')
|
|
215
|
+
if exited(p):
|
|
216
|
+
return None
|
|
208
217
|
if any(proof.get(key) != value for key, value in observed.get(str(p), {}).items()):
|
|
209
218
|
raise RuntimeError(f'{_name(entry)} process {p} identity/ancestry changed during discovery; source was not changed.')
|
|
210
219
|
observed[str(p)] = proof
|
|
@@ -219,6 +228,8 @@ def snapshot(entry, *, records=None, expected=None):
|
|
|
219
228
|
_, final = inventory()
|
|
220
229
|
if live & departed or remaining != live or final != live:
|
|
221
230
|
continue
|
|
231
|
+
if departed:
|
|
232
|
+
entry['departed_processes'] = {str(p): observed.get(str(p), {'parent': seen_parents[p]}) for p in departed}
|
|
222
233
|
return proofs
|
|
223
234
|
raise RuntimeError(f'{_name(entry)} process inventory kept changing after {SNAPSHOT_ATTEMPTS} samples; retry when descendants settle. Source was not changed and recovery remains owed.')
|
|
224
235
|
|
|
@@ -403,8 +414,23 @@ def _service_pid(entry):
|
|
|
403
414
|
|
|
404
415
|
def verify_processes(entry, records, *, stopping=False, fenced=False):
|
|
405
416
|
from gateway.status import get_process_start_time, _pid_exists
|
|
417
|
+
if not stopping:
|
|
418
|
+
# Reuse discovery's bounded proof without granting permission to lose an anchor.
|
|
419
|
+
candidate = dict(entry)
|
|
420
|
+
for boundary in range(2):
|
|
421
|
+
_, service_pid = _service_pid(entry)
|
|
422
|
+
if service_pid != entry['service_pid']:
|
|
423
|
+
raise RuntimeError(f'{_name(entry)} supervisor respawned during quiescence; source was not changed.')
|
|
424
|
+
if 'config_stamp' in entry and _config_stamp(entry) != entry['config_stamp']:
|
|
425
|
+
raise RuntimeError(f'{_name(entry)} service configuration changed; recovery remains owed.')
|
|
426
|
+
if boundary == 0:
|
|
427
|
+
candidate['processes'] = snapshot(candidate, records=records, expected=entry['processes'])
|
|
428
|
+
entry.update(processes=candidate['processes'])
|
|
429
|
+
if candidate.get('departed_processes'):
|
|
430
|
+
entry['departed_processes'] = candidate['departed_processes']
|
|
431
|
+
return True
|
|
406
432
|
parents = _process_table()
|
|
407
|
-
departed = set()
|
|
433
|
+
departed = set(map(int, entry.get('departed_processes', {})))
|
|
408
434
|
for pid, expected in entry['processes'].items():
|
|
409
435
|
pid = int(pid)
|
|
410
436
|
if _pid_exists(pid) is False:
|
|
@@ -442,7 +468,6 @@ def verify_processes(entry, records, *, stopping=False, fenced=False):
|
|
|
442
468
|
|
|
443
469
|
def bind_processes(entry, records):
|
|
444
470
|
if entry.get('processes'):
|
|
445
|
-
entry['processes'] = snapshot(entry, records=records, expected=entry['processes'])
|
|
446
471
|
verify_processes(entry, records)
|
|
447
472
|
|
|
448
473
|
|
|
@@ -713,8 +738,20 @@ class ServiceRecovery:
|
|
|
713
738
|
or ('runtime_revision' in transaction and not re.fullmatch(r'[a-f0-9]{40}', str(transaction['runtime_revision'])))):
|
|
714
739
|
raise RuntimeError('Service recovery transaction has an unknown identity or format; its evidence is retained.')
|
|
715
740
|
|
|
741
|
+
operation = self.state.get('update_operation')
|
|
742
|
+
if operation is not None and (
|
|
743
|
+
not isinstance(operation, dict)
|
|
744
|
+
or not re.fullmatch(r'[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}', str(operation.get('operation_id', '')))
|
|
745
|
+
or any(not re.fullmatch(r'[a-f0-9]{40}', str(operation.get(key, ''))) for key in ('baseline_revision', 'target_revision'))
|
|
746
|
+
or ('attempt_error' in operation and (not isinstance(operation['attempt_error'], str) or len(operation['attempt_error']) > 2000))
|
|
747
|
+
or ('source_moved' in operation and not isinstance(operation['source_moved'], bool))
|
|
748
|
+
or ('failed_stage' in operation and operation['failed_stage'] not in {'install', 'gateway_restart', 'agent_ready'})
|
|
749
|
+
or operation.get('stage') not in {'install', 'gateway_restart', 'agent_ready'}
|
|
750
|
+
or ('recovered_revision' in operation and not re.fullmatch(r'[a-f0-9]{40}', str(operation['recovered_revision'])))):
|
|
751
|
+
raise RuntimeError('Update operation correlation is malformed; its evidence is retained.')
|
|
752
|
+
|
|
716
753
|
def save(self, token=None):
|
|
717
|
-
if not self.state['entries'] and not self.state.get('sync_failed') and not self.state.get('transaction'):
|
|
754
|
+
if not self.state['entries'] and not self.state.get('sync_failed') and not self.state.get('transaction') and not self.state.get('update_operation'):
|
|
718
755
|
if self.path.exists():
|
|
719
756
|
self.path.unlink()
|
|
720
757
|
self._sync_directory()
|
|
@@ -734,9 +771,9 @@ class ServiceRecovery:
|
|
|
734
771
|
with contextlib.suppress(FileNotFoundError):
|
|
735
772
|
os.unlink(temporary)
|
|
736
773
|
|
|
737
|
-
def _sync_directory(self):
|
|
774
|
+
def _sync_directory(self, directory=None):
|
|
738
775
|
if os.name != 'nt':
|
|
739
|
-
fd = os.open(self.path.parent, os.O_RDONLY)
|
|
776
|
+
fd = os.open(directory if directory is not None else self.path.parent, os.O_RDONLY)
|
|
740
777
|
try:
|
|
741
778
|
os.fsync(fd)
|
|
742
779
|
finally:
|
|
@@ -767,10 +804,17 @@ class ServiceRecovery:
|
|
|
767
804
|
self.records.write(token)
|
|
768
805
|
self.save(token)
|
|
769
806
|
|
|
807
|
+
def verify_processes(self, entry, **permissions):
|
|
808
|
+
owed = next(e for e in self.state['entries'] if _service_key(e) == _service_key(entry))
|
|
809
|
+
result = verify_processes(owed, self.records, **permissions)
|
|
810
|
+
self.save() # Updated custody must precede the next lifecycle effect.
|
|
811
|
+
return result
|
|
812
|
+
|
|
770
813
|
def stop(self, entry, stopped):
|
|
771
814
|
owed = next(e for e in self.state['entries'] if _service_key(e) == _service_key(entry))
|
|
772
|
-
verify_processes(owed
|
|
815
|
+
self.verify_processes(owed)
|
|
773
816
|
backend_request(owed)
|
|
817
|
+
self.verify_processes(owed)
|
|
774
818
|
permit = backend_request(owed, 'prepare')
|
|
775
819
|
if permit.get('ok') is not True or permit.get('idle') is not True or not isinstance(permit.get('token'), str):
|
|
776
820
|
if owed['phase'] == 'recorded':
|
|
@@ -783,7 +827,7 @@ class ServiceRecovery:
|
|
|
783
827
|
except BaseException:
|
|
784
828
|
backend_request(owed, 'cancel', permit['token'])
|
|
785
829
|
raise
|
|
786
|
-
verify_processes(owed
|
|
830
|
+
self.verify_processes(owed)
|
|
787
831
|
if backend_request(owed, 'commit', permit['token']).get('ok') is not True:
|
|
788
832
|
raise RuntimeError(f'{_name(owed)} did not commit its backend retirement; recovery remains owed.')
|
|
789
833
|
owed['backend_committed'] = True
|
|
@@ -794,14 +838,14 @@ class ServiceRecovery:
|
|
|
794
838
|
# Disarm through the original manager before its KeepAlive can admit a successor.
|
|
795
839
|
owed['backend_stop_requested'] = True
|
|
796
840
|
self.save()
|
|
797
|
-
verify_processes(owed,
|
|
841
|
+
self.verify_processes(owed, stopping=stopping)
|
|
798
842
|
if _config_stamp(owed) != owed['config_stamp']:
|
|
799
843
|
raise RuntimeError(f'{_name(owed)} service configuration changed before backend stop; recovery remains owed.')
|
|
800
844
|
if stopped.is_set():
|
|
801
845
|
raise RuntimeError(f'{_name(owed)} backend stop was cancelled; recovery remains owed.')
|
|
802
846
|
self.pause._disarm(service_entry(owed))
|
|
803
847
|
wait_stopped(owed, self.records, stopped)
|
|
804
|
-
verify_processes(owed,
|
|
848
|
+
self.verify_processes(owed, stopping=True)
|
|
805
849
|
owed['phase'] = 'stopped'
|
|
806
850
|
self.save()
|
|
807
851
|
|
|
@@ -811,15 +855,15 @@ class ServiceRecovery:
|
|
|
811
855
|
raise RuntimeError(f'{_name(entry)} legacy shutdown remains owed; no signal was replayed.')
|
|
812
856
|
if _config_stamp(owed) != owed['config_stamp']:
|
|
813
857
|
raise RuntimeError(f'{_name(entry)} configuration changed before legacy refresh.')
|
|
814
|
-
verify_processes(owed
|
|
858
|
+
self.verify_processes(owed)
|
|
815
859
|
verify_listener(owed)
|
|
816
860
|
owed.update(legacy_refresh=True, phase='refreshing')
|
|
817
861
|
self.save() # Older readers reject this phase and retain debt; no reader replays its signal.
|
|
818
|
-
verify_processes(owed
|
|
862
|
+
self.verify_processes(owed)
|
|
819
863
|
verify_listener(owed)
|
|
820
864
|
if _config_stamp(owed) != owed['config_stamp']:
|
|
821
865
|
raise RuntimeError(f'{_name(owed)} configuration changed before legacy refresh.')
|
|
822
|
-
verify_processes(owed
|
|
866
|
+
self.verify_processes(owed)
|
|
823
867
|
try:
|
|
824
868
|
if stopped.is_set():
|
|
825
869
|
raise InterruptedError('Legacy refresh was cancelled before interruption.')
|
|
@@ -883,6 +927,8 @@ class ServiceRecovery:
|
|
|
883
927
|
if (_config_stamp(candidate) != entry['config_stamp']
|
|
884
928
|
or candidate.get('profile') != entry.get('profile')):
|
|
885
929
|
raise RuntimeError(f'{_name(entry)} successor configuration/profile changed; recovery remains owed.')
|
|
930
|
+
if entry.get('departed_processes'):
|
|
931
|
+
candidate['departed_processes'] = entry['departed_processes']
|
|
886
932
|
bind_processes(candidate, self.records)
|
|
887
933
|
parents = {int(pid): p['parent'] for pid, p in candidate['processes'].items()}
|
|
888
934
|
chain, pid = set(), candidate['pid']
|
|
@@ -894,7 +940,8 @@ class ServiceRecovery:
|
|
|
894
940
|
if chain | _descendants(candidate['pid'], parents) != set(parents):
|
|
895
941
|
raise RuntimeError(f'{_name(entry)} successor has unrelated descendants; recovery remains owed.')
|
|
896
942
|
for pid, process in candidate['processes'].items():
|
|
897
|
-
old = entry['processes'].get(pid) or entry.get('
|
|
943
|
+
old = (entry['processes'].get(pid) or entry.get('departed_processes', {}).get(pid)
|
|
944
|
+
or entry.get('retired_processes', {}).get(pid))
|
|
898
945
|
if old is not None and old != process:
|
|
899
946
|
raise RuntimeError(f'{_name(entry)} successor reused a recorded process identity; recovery remains owed.')
|
|
900
947
|
return candidate
|
|
@@ -966,7 +1013,7 @@ class ServiceRecovery:
|
|
|
966
1013
|
if pending:
|
|
967
1014
|
stopped.wait(.05)
|
|
968
1015
|
continue
|
|
969
|
-
if not verify_processes(entry,
|
|
1016
|
+
if not self.verify_processes(entry, stopping=True, fenced=True):
|
|
970
1017
|
stopped.wait(.05)
|
|
971
1018
|
continue
|
|
972
1019
|
if self._gateway_writers_live(entry) or _config_stamp(entry) != entry['config_stamp']:
|
|
@@ -989,7 +1036,7 @@ class ServiceRecovery:
|
|
|
989
1036
|
entry.update(candidate, retired_processes=retired, phase='retiring',
|
|
990
1037
|
drain_acknowledged=False, drain_acknowledged_at=None, settle_attempts=attempts)
|
|
991
1038
|
self.save() # Successor custody precedes its cooperative lifecycle request.
|
|
992
|
-
verify_processes(entry
|
|
1039
|
+
self.verify_processes(entry)
|
|
993
1040
|
if _config_stamp(entry) != entry['config_stamp']:
|
|
994
1041
|
raise RuntimeError(f'{_name(entry)} service configuration changed; recovery remains owed.')
|
|
995
1042
|
if not self.pause._ask_to_drain(entry):
|
|
@@ -1036,7 +1083,7 @@ class ServiceRecovery:
|
|
|
1036
1083
|
raise RuntimeError(f'{_name(entry)} uncommitted retirement could not yet be cancelled; recovery remains owed.')
|
|
1037
1084
|
self.stop(entry, threading.Event())
|
|
1038
1085
|
elif any(self.records.identity_is_live({'pid': int(pid), 'ct': p['ct']}) for pid, p in entry['processes'].items()):
|
|
1039
|
-
verify_processes(entry,
|
|
1086
|
+
self.verify_processes(entry, stopping=True)
|
|
1040
1087
|
if any(self.records.identity_is_live({'pid': int(pid), 'ct': p['ct']}) for pid, p in entry['processes'].items() if int(pid) != entry['service_pid']):
|
|
1041
1088
|
raise RuntimeError(f'{_name(entry)} still has live writer descendants; recovery remains owed.')
|
|
1042
1089
|
self._stop_backend_supervisor(entry, threading.Event(), stopping=True)
|
|
@@ -1062,6 +1109,18 @@ class ServiceRecovery:
|
|
|
1062
1109
|
raise RuntimeError(f'{_name(entry)} readiness is unverified at its original endpoint; recovery remains owed.')
|
|
1063
1110
|
time.sleep(.1)
|
|
1064
1111
|
|
|
1112
|
+
def _verify_candidate(self, candidate, entry):
|
|
1113
|
+
# Readiness retries must retain both live observations and proved departures.
|
|
1114
|
+
candidate['config_stamp'] = entry['config_stamp']
|
|
1115
|
+
candidate['processes'] = {**candidate['processes'], **entry.get('readiness_processes', {})}
|
|
1116
|
+
candidate['departed_processes'] = {**entry.get('departed_processes', {}),
|
|
1117
|
+
**candidate.get('departed_processes', {})}
|
|
1118
|
+
bind_processes(candidate, self.records)
|
|
1119
|
+
entry['readiness_processes'] = candidate['processes']
|
|
1120
|
+
if candidate['departed_processes']:
|
|
1121
|
+
entry['departed_processes'] = candidate['departed_processes']
|
|
1122
|
+
self.save()
|
|
1123
|
+
|
|
1065
1124
|
def _ready(self, entry, ledger):
|
|
1066
1125
|
if _config_stamp(entry) != entry['config_stamp']:
|
|
1067
1126
|
raise RuntimeError(f'{_name(entry)} service configuration changed; backend recovery remains owed.')
|
|
@@ -1075,8 +1134,9 @@ class ServiceRecovery:
|
|
|
1075
1134
|
candidate = self._gateway_candidate(entry)
|
|
1076
1135
|
if candidate is None:
|
|
1077
1136
|
return False
|
|
1137
|
+
self._verify_candidate(candidate, entry)
|
|
1078
1138
|
ready = _gateway_home_matches(entry['home'], (candidate['pid'], candidate['ct']), self.pause)
|
|
1079
|
-
|
|
1139
|
+
self._verify_candidate(candidate, entry)
|
|
1080
1140
|
return ready
|
|
1081
1141
|
if entry.get('legacy_refresh') and any(self.records.identity_is_live({'pid': int(pid), 'ct': p['ct']})
|
|
1082
1142
|
for pid, p in entry['processes'].items()
|
|
@@ -1095,7 +1155,7 @@ class ServiceRecovery:
|
|
|
1095
1155
|
if (candidate['profile'] != entry['profile'] or _service_key(candidate) != _service_key(entry)
|
|
1096
1156
|
or _config_stamp(candidate) != entry['config_stamp']):
|
|
1097
1157
|
return False
|
|
1098
|
-
|
|
1158
|
+
self._verify_candidate(candidate, entry)
|
|
1099
1159
|
try:
|
|
1100
1160
|
if entry.get('legacy_refresh'):
|
|
1101
1161
|
verify_listener(candidate)
|
|
@@ -1106,7 +1166,7 @@ class ServiceRecovery:
|
|
|
1106
1166
|
return False
|
|
1107
1167
|
if backend_request(candidate, 'cancel', permit['token']).get('ok') is not True:
|
|
1108
1168
|
return False
|
|
1109
|
-
|
|
1169
|
+
self._verify_candidate(candidate, entry)
|
|
1110
1170
|
if self.identity._pid_alive_matches(row['pid'], row['create_time'], strict=True) is not True:
|
|
1111
1171
|
return False
|
|
1112
1172
|
except BackendLifecycleUnsupported:
|
|
@@ -29,9 +29,9 @@ class Registry:
|
|
|
29
29
|
|
|
30
30
|
def build_methods(runtime: AgentRuntime) -> dict[str, Handler]:
|
|
31
31
|
registry = Registry(runtime)
|
|
32
|
-
from . import aux, chat, checkpoints, commands, config, connectors, cron, dashboard_plugins, gateway, goals, kanban, mcp, memory, oauth, plugin_oauth, process, profiles, providers, skills, state_db, stt, text, toolsets, usage, worktree, write_approval # noqa: WPS433
|
|
32
|
+
from . import aux, chat, checkpoints, commands, config, connectors, cron, dashboard_plugins, gateway, goals, kanban, mcp, managed_buffers, memory, oauth, outbound_drafts, plugin_oauth, process, profiles, providers, skills, state_db, stt, text, toolsets, usage, worktree, write_approval # noqa: WPS433
|
|
33
33
|
from . import runtime as runtime_methods
|
|
34
34
|
|
|
35
|
-
for module in (runtime_methods, goals, commands, kanban, state_db, profiles, skills, mcp, stt, cron, providers, oauth, plugin_oauth, aux, text, process, usage, gateway, worktree, checkpoints, memory, chat, config, toolsets, write_approval, connectors, dashboard_plugins):
|
|
35
|
+
for module in (runtime_methods, goals, commands, kanban, state_db, profiles, skills, mcp, stt, cron, providers, oauth, plugin_oauth, aux, text, process, usage, gateway, worktree, checkpoints, memory, chat, config, toolsets, write_approval, connectors, dashboard_plugins, outbound_drafts, managed_buffers):
|
|
36
36
|
module.register(registry)
|
|
37
37
|
return registry.methods
|
|
@@ -190,15 +190,15 @@ def _plugins(home):
|
|
|
190
190
|
return out
|
|
191
191
|
|
|
192
192
|
|
|
193
|
-
def _named(home, name):
|
|
194
|
-
return next((p for p in _plugins(home) if p['name'] == name and p['enabled'] and p['panel']), None)
|
|
193
|
+
def _named(home, name, *, require_panel=True):
|
|
194
|
+
return next((p for p in _plugins(home) if p['name'] == name and p['enabled'] and (p['panel'] if require_panel else p.get('_api_file'))), None)
|
|
195
195
|
|
|
196
196
|
|
|
197
197
|
def _reply(status=404, body=b'', content_type='application/octet-stream'):
|
|
198
198
|
return {'status': status, 'body': base64.b64encode(body).decode('ascii'), 'content_type': content_type}
|
|
199
199
|
|
|
200
200
|
|
|
201
|
-
def _request(ctx, plugin, params):
|
|
201
|
+
def _request(ctx, plugin, params, *, require_panel=True):
|
|
202
202
|
ctx.check_cancelled()
|
|
203
203
|
if params.get('authenticated') is not True:
|
|
204
204
|
return _reply()
|
|
@@ -240,7 +240,7 @@ def _request(ctx, plugin, params):
|
|
|
240
240
|
content_type = next((v for k, v in result.get('headers', []) if k.lower() == 'content-type'), 'application/octet-stream')
|
|
241
241
|
if 'text/event-stream' in content_type.lower():
|
|
242
242
|
return _reply(501, b'Agent plugin host supports buffered HTTP only', 'text/plain; charset=utf-8')
|
|
243
|
-
if _named(homes.profile_home_param(params), plugin['name']) is None:
|
|
243
|
+
if _named(homes.profile_home_param(params), plugin['name'], require_panel=require_panel) is None:
|
|
244
244
|
return _reply()
|
|
245
245
|
raw = result.get('body', b'')
|
|
246
246
|
if not isinstance(raw, bytes) or len(raw) > _MAX_API:
|
|
@@ -17,8 +17,9 @@ from ..errors import InvalidParams
|
|
|
17
17
|
from ..home import profile_home_param, scoped_home
|
|
18
18
|
|
|
19
19
|
_ACTIONS = {"snapshot", "watch", "context_detail", "submit", "receipt", "cancel", "steer_queued", "replace_queued", "interrupt", "approval", "clarify",
|
|
20
|
-
"clarify_lock", "discard", "mutate", "export"}
|
|
21
|
-
|
|
20
|
+
"clarify_lock", "discard", "mutate", "export", "checkpoint_diff", "terminal_info", "shell_exec", "react"}
|
|
21
|
+
_OWNER_READ_MAX_BYTES = 4 * 1024 * 1024
|
|
22
|
+
_REACTION_EMOJIS = {"👍", "👎", "❤️", "😂", "😮", "🎉"}
|
|
22
23
|
# A restart's after-turn wait keeps the owner reachable only to finish its parked turn (the owner's
|
|
23
24
|
# RESTART_WAIT_WS_METHODS); new work waits for the next owner.
|
|
24
25
|
_RESTART_ACTIONS = {"snapshot", "watch", "receipt", "approval", "clarify", "clarify_lock", "interrupt"}
|
|
@@ -75,8 +76,15 @@ def _snapshot(value: dict) -> dict:
|
|
|
75
76
|
message_keys = {"role", "content", "tool_calls", "tool_call_id", "name", "timestamp", "message_id", "row_id",
|
|
76
77
|
"reasoning", "reasoning_content", "display_kind", "finish_reason"}
|
|
77
78
|
info_keys = {"title", "model", "cwd", "source", "provider", "config_warning"}
|
|
78
|
-
|
|
79
|
-
|
|
79
|
+
rows = []
|
|
80
|
+
for row in value["messages"]:
|
|
81
|
+
metadata = row.get("display_metadata")
|
|
82
|
+
reactions = metadata.get("reactions", []) if isinstance(metadata, dict) else []
|
|
83
|
+
rows.append({**{k: v for k, v in row.items() if k in message_keys}, "reactions": [
|
|
84
|
+
{"author": reaction["author"], "emoji": reaction["emoji"]} for reaction in reactions
|
|
85
|
+
if isinstance(reaction, dict) and reaction.get("author") in ("user", "agent")
|
|
86
|
+
and isinstance(reaction.get("emoji"), str) and reaction["emoji"] in _REACTION_EMOJIS] if isinstance(reactions, list) else []})
|
|
87
|
+
return {**value, "messages": rows, "info": {k: v for k, v in value["info"].items() if k in info_keys}}
|
|
80
88
|
|
|
81
89
|
|
|
82
90
|
def _refused(state: str, reason: str) -> dict:
|
|
@@ -120,16 +128,20 @@ def _served(home: Path, endpoint, *, allow_verified: bool = False) -> bool:
|
|
|
120
128
|
return False
|
|
121
129
|
if not allow_verified:
|
|
122
130
|
return True
|
|
131
|
+
return not _verified_contract(endpoint, "served-local-policy-v1")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _verified_contract(endpoint, name: str) -> bool:
|
|
123
135
|
# The private control socket reports the owner's boot revision, not the sidecar's
|
|
124
136
|
# checkout or a version label. The authenticated describe must still match below.
|
|
125
137
|
try:
|
|
126
138
|
record = json.loads((Path(__file__).resolve().parents[2] / "agent_support.json").read_text())
|
|
127
|
-
revisions = record["nativeContracts"][
|
|
128
|
-
return (
|
|
129
|
-
|
|
130
|
-
|
|
139
|
+
revisions = record["nativeContracts"][name]
|
|
140
|
+
return (isinstance(revisions, list)
|
|
141
|
+
and all(isinstance(sha, str) and re.fullmatch(r"[0-9a-f]{40}", sha) for sha in revisions)
|
|
142
|
+
and getattr(endpoint, "code_sha", None) in revisions)
|
|
131
143
|
except (OSError, ValueError, KeyError, TypeError):
|
|
132
|
-
return
|
|
144
|
+
return False
|
|
133
145
|
|
|
134
146
|
|
|
135
147
|
def _owner_refusal(described: dict, endpoint) -> dict | None:
|
|
@@ -145,12 +157,41 @@ def _owner_refusal(described: dict, endpoint) -> dict | None:
|
|
|
145
157
|
return None
|
|
146
158
|
|
|
147
159
|
|
|
160
|
+
def _checkpoint_read(value, *, no_baseline=False):
|
|
161
|
+
comparison = {"provenance": "native_owner", "baseline": "earliest_retained_project_checkpoint",
|
|
162
|
+
"available": False, "reason": None, "truncated": False}
|
|
163
|
+
result = {"diff": "", "stat": "", "comparison": comparison}
|
|
164
|
+
reason = None
|
|
165
|
+
if not isinstance(value, dict) or type(value.get("enabled")) is not bool:
|
|
166
|
+
reason = "invalid_payload"
|
|
167
|
+
elif not value["enabled"]:
|
|
168
|
+
reason = "checkpoints_disabled"
|
|
169
|
+
elif value.get("success") is not True:
|
|
170
|
+
reason = "checkpoint_unavailable"
|
|
171
|
+
elif (not isinstance(value.get("diff"), str) or not isinstance(value.get("stat"), str)
|
|
172
|
+
or type(value.get("diff_lines")) is not int or value["diff_lines"] < 0
|
|
173
|
+
or type(value.get("empty")) is not bool):
|
|
174
|
+
reason = "invalid_payload"
|
|
175
|
+
elif sum(len(json.dumps(value[key], ensure_ascii=False).encode("utf-8", "surrogatepass")) - 2
|
|
176
|
+
for key in ("stat", "diff")) > _OWNER_READ_MAX_BYTES:
|
|
177
|
+
reason = "payload_too_large"
|
|
178
|
+
elif no_baseline:
|
|
179
|
+
reason = "no_baseline"
|
|
180
|
+
else:
|
|
181
|
+
result.update(diff=value["diff"], stat=value["stat"])
|
|
182
|
+
comparison.update(available=True, truncated=value["diff_lines"] > len(value["diff"].splitlines()))
|
|
183
|
+
comparison["reason"] = reason
|
|
184
|
+
return result
|
|
185
|
+
|
|
186
|
+
|
|
148
187
|
async def _run(ctx, params: dict, home: Path, discovery) -> dict:
|
|
149
188
|
from hermes_cli.gateway_client import GatewayRPCError
|
|
150
189
|
|
|
151
190
|
endpoint = discovery.endpoint
|
|
152
191
|
if _served(home, endpoint, allow_verified=True):
|
|
153
192
|
return _refused("incompatible", "served_profile_sessions_unavailable")
|
|
193
|
+
if params["action"] in {"terminal_info", "shell_exec"} and not _verified_contract(endpoint, "captured-command-v1"):
|
|
194
|
+
return _refused("incompatible", "owner_shell_unsupported")
|
|
154
195
|
restarting = discovery.state != "ready"
|
|
155
196
|
if restarting and params["action"] not in _RESTART_ACTIONS:
|
|
156
197
|
return _refused("refused", "gateway_restarting")
|
|
@@ -170,10 +211,22 @@ async def _run(ctx, params: dict, home: Path, discovery) -> dict:
|
|
|
170
211
|
subscription = value["subscription_id"]
|
|
171
212
|
if reason := await asyncio.to_thread(_control_refusal, home, sid):
|
|
172
213
|
raise GatewayRPCError(reason)
|
|
173
|
-
return _snapshot(value)
|
|
214
|
+
return {**_snapshot(value), "reactions_supported": _verified_contract(endpoint, "message-reactions-v1")}
|
|
174
215
|
|
|
175
216
|
receipt = lock = None
|
|
176
217
|
try:
|
|
218
|
+
if params["action"] == "checkpoint_diff":
|
|
219
|
+
# The owner resolves the physical target and frozen policy. Never resume or read host files.
|
|
220
|
+
value = await asyncio.wait_for(client.rpc("rollback.session_diff", session_id=sid, _timeout=25), timeout=30)
|
|
221
|
+
read = _checkpoint_read(value)
|
|
222
|
+
if read["comparison"]["available"] and value["empty"] and not value["diff"] and not value["stat"]:
|
|
223
|
+
listed = await asyncio.wait_for(client.rpc("rollback.list", session_id=sid, _timeout=3), timeout=3)
|
|
224
|
+
if (not isinstance(listed, dict) or listed.get("enabled") is not True
|
|
225
|
+
or not isinstance(listed.get("checkpoints"), list)):
|
|
226
|
+
read = _checkpoint_read(None)
|
|
227
|
+
elif not listed["checkpoints"]:
|
|
228
|
+
read = _checkpoint_read(value, no_baseline=True)
|
|
229
|
+
return {"state": "attached", "reason": None, "snapshot": None, "receipt": None, "checkpoint_diff": read}
|
|
177
230
|
if params["action"] == "export":
|
|
178
231
|
# No resume/subscription or mutation: the owner selects its current physical target.
|
|
179
232
|
fmt = "md" if params["format"] == "markdown" else params["format"]
|
|
@@ -183,14 +236,34 @@ async def _run(ctx, params: dict, home: Path, discovery) -> dict:
|
|
|
183
236
|
or not isinstance(value.get("filename"), str) or not 0 < len(value["filename"]) <= 255
|
|
184
237
|
or not isinstance(value.get("content"), str)):
|
|
185
238
|
return _refused("refused", "owner_export_invalid")
|
|
186
|
-
if len(value["content"]) >
|
|
239
|
+
if len(value["content"]) > _OWNER_READ_MAX_BYTES or len(value["content"].encode("utf-8")) > _OWNER_READ_MAX_BYTES:
|
|
187
240
|
return _refused("refused", "export_too_large")
|
|
188
241
|
return {"state": "attached", "reason": None, "snapshot": None, "receipt": None,
|
|
189
242
|
"export": {"format": params["format"], "filename": value["filename"], "content": value["content"]}}
|
|
190
243
|
snap = await snapshot()
|
|
191
244
|
action = params["action"]
|
|
192
245
|
common = {"session_id": sid}
|
|
193
|
-
if action == "
|
|
246
|
+
if action == "terminal_info":
|
|
247
|
+
return {"state": "attached", "reason": None, "snapshot": snap, "receipt": None}
|
|
248
|
+
if action == "shell_exec":
|
|
249
|
+
if (snap.get("session_id") != params["session_id"] or snap.get("authority_epoch") != params["authority_epoch"]
|
|
250
|
+
or snap.get("replay_epoch") != params["replay_epoch"]):
|
|
251
|
+
return _refused("refused", "owner_changed")
|
|
252
|
+
# The native owner alone authorizes the frozen backend/cwd and redacts before bounding.
|
|
253
|
+
# No second observation may discard a received result; no retry after an unknown ACK.
|
|
254
|
+
value = await client.rpc("shell.exec", **common, command=params["command"], _timeout=35)
|
|
255
|
+
if (not isinstance(value, dict) or not isinstance(value.get("stdout"), str)
|
|
256
|
+
or not isinstance(value.get("stderr"), str) or type(value.get("code")) is not int
|
|
257
|
+
or len(value["stdout"]) > 4000 or len(value["stderr"]) > 2000):
|
|
258
|
+
return _refused("refused", "owner_shell_invalid")
|
|
259
|
+
return {"state": "attached", "reason": None, "snapshot": None, "receipt": None, "shell": value}
|
|
260
|
+
if action == "react":
|
|
261
|
+
if not snap["reactions_supported"]:
|
|
262
|
+
return _refused("refused", "owner_reactions_unsupported")
|
|
263
|
+
if not any(row.get("row_id") == params["row_id"] and row.get("role") in {"user", "assistant"} for row in snap["messages"]):
|
|
264
|
+
return _refused("refused", "reaction_row_unavailable")
|
|
265
|
+
await client.rpc("message.react", **common, row_id=params["row_id"], emoji=params["emoji"], author="user")
|
|
266
|
+
elif action == "context_detail":
|
|
194
267
|
from ..context_detail import project, unavailable
|
|
195
268
|
# The authorized native read owns lookup and prompt interpretation. No local Agent is built.
|
|
196
269
|
try:
|
|
@@ -264,16 +337,22 @@ async def _run(ctx, params: dict, home: Path, discovery) -> dict:
|
|
|
264
337
|
ctx.emit("replay_complete", {})
|
|
265
338
|
checked = asyncio.get_running_loop().time()
|
|
266
339
|
while not ctx.cancelled:
|
|
340
|
+
now = asyncio.get_running_loop().time()
|
|
341
|
+
if not restarting and now - checked >= 2:
|
|
342
|
+
checked = now
|
|
343
|
+
if restarting := await asyncio.to_thread(_restart_wait, home):
|
|
344
|
+
result["restarting"] = True
|
|
345
|
+
ctx.emit("restarting", {"restarting": True})
|
|
346
|
+
if not restarting and snap.get("reactions_supported"):
|
|
347
|
+
# The owner emits no reaction event. Read through this viewer's existing subscription,
|
|
348
|
+
# including while tokens keep the event queue busy.
|
|
349
|
+
current = await snapshot()
|
|
350
|
+
if [(r.get("row_id"), r.get("reactions")) for r in current["messages"] if r.get("reactions")] != [(r.get("row_id"), r.get("reactions")) for r in snap["messages"] if r.get("reactions")]:
|
|
351
|
+
ctx.emit("snapshot", {**result, "snapshot": current, "reaction_only": True})
|
|
352
|
+
snap = current
|
|
267
353
|
try:
|
|
268
354
|
frame = await asyncio.wait_for(events.get(), .25)
|
|
269
355
|
except TimeoutError:
|
|
270
|
-
now = asyncio.get_running_loop().time()
|
|
271
|
-
if not restarting and now - checked >= 2:
|
|
272
|
-
checked = now
|
|
273
|
-
# One-way for this owner: its next state is exit, which ends the watch.
|
|
274
|
-
if restarting := await asyncio.to_thread(_restart_wait, home):
|
|
275
|
-
result["restarting"] = True
|
|
276
|
-
ctx.emit("restarting", {"restarting": True})
|
|
277
356
|
continue
|
|
278
357
|
if isinstance(frame, Exception):
|
|
279
358
|
raise frame
|
|
@@ -310,6 +389,8 @@ async def _run(ctx, params: dict, home: Path, discovery) -> dict:
|
|
|
310
389
|
ctx.emit("event", event)
|
|
311
390
|
return result
|
|
312
391
|
except GatewayRPCError as exc:
|
|
392
|
+
if params["action"] == "react" and str(exc) not in {"invalid_params", "not_found", "permission_denied", "profile_mismatch"}:
|
|
393
|
+
return _refused("unavailable", "reaction_outcome_unknown")
|
|
313
394
|
if params["action"] == "export":
|
|
314
395
|
return _refused("refused", "export_too_large" if str(exc) == "export_too_large" else "owner_export_unsupported")
|
|
315
396
|
return {**_refused("incompatible" if str(exc) in _CONTROL_REFUSALS else "refused", str(exc)), "receipt": receipt}
|
|
@@ -330,9 +411,18 @@ def session(ctx, params: dict) -> dict:
|
|
|
330
411
|
if action not in _ACTIONS or not isinstance(sid, str) or not sid.strip() or sid != sid.strip():
|
|
331
412
|
raise InvalidParams("session_id and supported action are required")
|
|
332
413
|
params = {**params, "action": action}
|
|
414
|
+
if action == "react" and (set(params) - {"profile_home", "session_id", "action", "row_id", "emoji"}
|
|
415
|
+
or type(params.get("row_id")) is not int or params["row_id"] <= 0
|
|
416
|
+
or "emoji" not in params or (params["emoji"] is not None and (not isinstance(params["emoji"], str) or params["emoji"] not in _REACTION_EMOJIS))):
|
|
417
|
+
raise InvalidParams("react requires a visible row and an allowed emoji")
|
|
333
418
|
if action == "export" and (params.get("format") not in ("json", "markdown", "html")
|
|
334
419
|
or not isinstance(params.get("redact"), bool)):
|
|
335
420
|
raise InvalidParams("export requires an allowed format and redaction policy")
|
|
421
|
+
if action == "shell_exec" and (not isinstance(params.get("command"), str)
|
|
422
|
+
or not params["command"].strip() or len(params["command"]) > 8192
|
|
423
|
+
or type(params.get("authority_epoch")) is not int or params["authority_epoch"] < 0
|
|
424
|
+
or not isinstance(params.get("replay_epoch"), str) or not params["replay_epoch"]):
|
|
425
|
+
raise InvalidParams("captured command requires command and owner identity")
|
|
336
426
|
required = {
|
|
337
427
|
"submit": ("request_id", "text"), "receipt": ("admission_id",), "cancel": ("admission_id",),
|
|
338
428
|
"steer_queued": ("admission_id",), "replace_queued": ("admission_id", "submission_id", "text"),
|
|
@@ -10,8 +10,11 @@ from __future__ import annotations
|
|
|
10
10
|
import functools
|
|
11
11
|
import json
|
|
12
12
|
import secrets
|
|
13
|
+
import sqlite3
|
|
13
14
|
import time
|
|
15
|
+
from contextlib import closing
|
|
14
16
|
from dataclasses import asdict, is_dataclass
|
|
17
|
+
from pathlib import Path
|
|
15
18
|
from typing import Any, NamedTuple
|
|
16
19
|
|
|
17
20
|
from ..errors import InvalidParams, RpcError
|
|
@@ -463,6 +466,50 @@ def _bool(params: dict, key: str, default: bool = False) -> bool:
|
|
|
463
466
|
|
|
464
467
|
# ── payload builders ──────────────────────────────────────────────────────
|
|
465
468
|
|
|
469
|
+
def inspection_payload(params: dict) -> dict:
|
|
470
|
+
"""Read only the selected profile's live boards, with shared global row/edge budgets."""
|
|
471
|
+
kb = _kb()
|
|
472
|
+
selected = _board(params)
|
|
473
|
+
boards = sorted((b for b in kb.list_boards(include_archived=False) if not b.get("archived")), key=lambda b: b["slug"])
|
|
474
|
+
if selected is not None:
|
|
475
|
+
boards = [b for b in boards if b["slug"] == selected]
|
|
476
|
+
if not boards:
|
|
477
|
+
raise NotFound("board not found")
|
|
478
|
+
nodes, edges = [], []
|
|
479
|
+
truncated = len(boards) > 32
|
|
480
|
+
for meta in boards[:32]:
|
|
481
|
+
slug = _normalize_existing_slug(meta["slug"])
|
|
482
|
+
if len(nodes) >= 500:
|
|
483
|
+
truncated = True
|
|
484
|
+
break
|
|
485
|
+
# An inspection must never initialize a missing database or run migrations.
|
|
486
|
+
uri = Path(kb.kanban_db_path(slug)).resolve().as_uri() + "?mode=ro"
|
|
487
|
+
with closing(sqlite3.connect(uri, uri=True)) as conn:
|
|
488
|
+
conn.row_factory = sqlite3.Row
|
|
489
|
+
conn.execute("BEGIN")
|
|
490
|
+
where = "" if _bool(params, "include_archived") else "WHERE t.status != 'archived'"
|
|
491
|
+
rows = conn.execute(
|
|
492
|
+
f"SELECT t.*, r.started_at AS current_run_started_at FROM tasks t LEFT JOIN task_runs r ON r.id = t.current_run_id {where} ORDER BY t.id LIMIT ?",
|
|
493
|
+
(501 - len(nodes),),
|
|
494
|
+
).fetchall()
|
|
495
|
+
remaining = 500 - len(nodes)
|
|
496
|
+
truncated = truncated or len(rows) > remaining
|
|
497
|
+
rows = rows[:remaining]
|
|
498
|
+
ids = [row["id"] for row in rows]
|
|
499
|
+
for row in rows:
|
|
500
|
+
task = _task_dict(dict(row), conn)
|
|
501
|
+
nodes.append({"key": f"{slug}/{row['id']}", "board": slug, "board_name": meta.get("name") or slug, "task": task})
|
|
502
|
+
if ids:
|
|
503
|
+
placeholders = ",".join("?" for _ in ids)
|
|
504
|
+
links = conn.execute(
|
|
505
|
+
f"SELECT parent_id, child_id FROM task_links WHERE parent_id IN ({placeholders}) AND child_id IN ({placeholders}) ORDER BY parent_id, child_id LIMIT ?",
|
|
506
|
+
[*ids, *ids, 2001 - len(edges)],
|
|
507
|
+
).fetchall()
|
|
508
|
+
remaining_edges = 2000 - len(edges)
|
|
509
|
+
truncated = truncated or len(links) > remaining_edges
|
|
510
|
+
edges.extend({"source": f"{slug}/{r['parent_id']}", "target": f"{slug}/{r['child_id']}"} for r in links[:remaining_edges])
|
|
511
|
+
return {"nodes": nodes, "edges": edges, "generated_at": time.time(), "truncated": truncated, "read_only": False}
|
|
512
|
+
|
|
466
513
|
def board_payload(params: dict) -> dict:
|
|
467
514
|
board = _board(params)
|
|
468
515
|
kb = _kb()
|
|
@@ -852,6 +899,7 @@ def normalize_board_payload(params: dict) -> dict:
|
|
|
852
899
|
|
|
853
900
|
|
|
854
901
|
METHODS = {
|
|
902
|
+
"kanban.inspection": inspection_payload,
|
|
855
903
|
"kanban.normalize_board": normalize_board_payload,
|
|
856
904
|
"kanban.board": board_payload,
|
|
857
905
|
"kanban.boards": list_boards_payload,
|