@alisio/server 0.1.0-alpha.1
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/LICENSE +21 -0
- package/README.md +47 -0
- package/dist/auth/guard.d.ts +37 -0
- package/dist/auth/guard.js +83 -0
- package/dist/auth/token.d.ts +6 -0
- package/dist/auth/token.js +22 -0
- package/dist/bridges/approval-bridge.d.ts +53 -0
- package/dist/bridges/approval-bridge.js +162 -0
- package/dist/bridges/interaction-bridge.d.ts +52 -0
- package/dist/bridges/interaction-bridge.js +163 -0
- package/dist/host/git.d.ts +22 -0
- package/dist/host/git.js +99 -0
- package/dist/host/presets.d.ts +21 -0
- package/dist/host/presets.js +60 -0
- package/dist/host/run-scheduler.d.ts +67 -0
- package/dist/host/run-scheduler.js +143 -0
- package/dist/host/sessions.d.ts +81 -0
- package/dist/host/sessions.js +211 -0
- package/dist/host/workspace-host.d.ts +87 -0
- package/dist/host/workspace-host.js +195 -0
- package/dist/http/body.d.ts +7 -0
- package/dist/http/body.js +33 -0
- package/dist/http/errors.d.ts +15 -0
- package/dist/http/errors.js +58 -0
- package/dist/http/router.d.ts +37 -0
- package/dist/http/router.js +62 -0
- package/dist/http/static.d.ts +27 -0
- package/dist/http/static.js +132 -0
- package/dist/index.d.ts +66 -0
- package/dist/index.js +350 -0
- package/dist/log.d.ts +21 -0
- package/dist/log.js +19 -0
- package/dist/routes/approvals.d.ts +8 -0
- package/dist/routes/approvals.js +33 -0
- package/dist/routes/blobs.d.ts +13 -0
- package/dist/routes/blobs.js +46 -0
- package/dist/routes/commands.d.ts +21 -0
- package/dist/routes/commands.js +149 -0
- package/dist/routes/events.d.ts +14 -0
- package/dist/routes/events.js +32 -0
- package/dist/routes/files.d.ts +21 -0
- package/dist/routes/files.js +262 -0
- package/dist/routes/health.d.ts +18 -0
- package/dist/routes/health.js +38 -0
- package/dist/routes/images.d.ts +12 -0
- package/dist/routes/images.js +60 -0
- package/dist/routes/management.d.ts +32 -0
- package/dist/routes/management.js +326 -0
- package/dist/routes/prompts.d.ts +22 -0
- package/dist/routes/prompts.js +153 -0
- package/dist/routes/providers.d.ts +3 -0
- package/dist/routes/providers.js +230 -0
- package/dist/routes/session-views.d.ts +12 -0
- package/dist/routes/session-views.js +76 -0
- package/dist/routes/sessions.d.ts +16 -0
- package/dist/routes/sessions.js +160 -0
- package/dist/routes/workspaces.d.ts +7 -0
- package/dist/routes/workspaces.js +32 -0
- package/dist/schemas.d.ts +25 -0
- package/dist/schemas.js +54 -0
- package/dist/sse/hub.d.ts +80 -0
- package/dist/sse/hub.js +287 -0
- package/dist/sse/inflight.d.ts +16 -0
- package/dist/sse/inflight.js +84 -0
- package/dist/web/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/dist/web/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/dist/web/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/dist/web/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/dist/web/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/dist/web/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/dist/web/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/dist/web/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/dist/web/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/dist/web/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/dist/web/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/dist/web/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/dist/web/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/dist/web/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/dist/web/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/dist/web/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/dist/web/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/dist/web/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/dist/web/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/dist/web/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/dist/web/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/dist/web/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/dist/web/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/dist/web/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/dist/web/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/dist/web/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/dist/web/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/dist/web/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/dist/web/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/dist/web/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/dist/web/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/dist/web/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/dist/web/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/dist/web/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/dist/web/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/dist/web/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/dist/web/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/dist/web/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/dist/web/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/dist/web/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/dist/web/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/dist/web/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/dist/web/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/dist/web/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/dist/web/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/dist/web/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/dist/web/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/dist/web/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/dist/web/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/dist/web/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/dist/web/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/dist/web/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/dist/web/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/dist/web/assets/KaTeX_Size3-Regular-gV2CO0n9.woff2 +0 -0
- package/dist/web/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/dist/web/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/dist/web/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/dist/web/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/dist/web/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/dist/web/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/dist/web/assets/SettingsModal-Dj5G3JIz.js +1 -0
- package/dist/web/assets/SettingsModal-YCahs6O5.css +1 -0
- package/dist/web/assets/abnfDiagram-VCTEODGH-Ct0-FsLe.js +1 -0
- package/dist/web/assets/arc-Pry-7G1_.js +1 -0
- package/dist/web/assets/architecture-7GRP2DOG-B5Qnm7io.js +1 -0
- package/dist/web/assets/architectureDiagram-5GKGNRK7-BvjnUlvj.js +36 -0
- package/dist/web/assets/array-BifhSqXX.js +1 -0
- package/dist/web/assets/blockDiagram-I7D4REHJ-CBHCj2Mn.js +129 -0
- package/dist/web/assets/c4Diagram-7LVT6UL2-9I13K7Yy.js +38 -0
- package/dist/web/assets/channel-B-TykkBN.js +1 -0
- package/dist/web/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
- package/dist/web/assets/chunk-4HAMMTFA-Du4xf36t.js +62 -0
- package/dist/web/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
- package/dist/web/assets/chunk-75Z2AOVW-93KpRXCT.js +2 -0
- package/dist/web/assets/chunk-DU6HZSFF-DP6dsL6K.js +125 -0
- package/dist/web/assets/chunk-F27PBJKO-CxLLRIN4.js +1 -0
- package/dist/web/assets/chunk-FOHPRMQF-DzxwRta7.js +161 -0
- package/dist/web/assets/chunk-GMAD6QVW-BGv500CR.js +72 -0
- package/dist/web/assets/chunk-GVQU2GXP-BmXv3St8.js +1 -0
- package/dist/web/assets/chunk-IMKFNOWR-tmbCLM1n.js +231 -0
- package/dist/web/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
- package/dist/web/assets/chunk-L3NEJ4N5-BULbKNtl.js +1 -0
- package/dist/web/assets/chunk-OSK3NFVY-DyvADXPn.js +10 -0
- package/dist/web/assets/chunk-P2QGCYS3-YhA2sAhR.js +1 -0
- package/dist/web/assets/chunk-POPQ4Y6H-BLr1KktA.js +1 -0
- package/dist/web/assets/chunk-PWAF6VOD-DJCZC9ie.js +1 -0
- package/dist/web/assets/chunk-SHT3W25Y-B8xH9z5t.js +168 -0
- package/dist/web/assets/chunk-SVP7TREG-DxynkM9y.js +88 -0
- package/dist/web/assets/chunk-TICWLB2K-CZPXqwaW.js +206 -0
- package/dist/web/assets/chunk-XXDRQBXY-DqAYeljN.js +1 -0
- package/dist/web/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
- package/dist/web/assets/classDiagram-ZZMXUADV-B6LKfKSk.js +1 -0
- package/dist/web/assets/classDiagram-v2-VYDZK3BY-B6LKfKSk.js +1 -0
- package/dist/web/assets/cose-bilkent-JH36ORCC-BEpcWcmK.js +1 -0
- package/dist/web/assets/css-BsVw1vtW.js +1 -0
- package/dist/web/assets/cynefin-OW5HDTMX-rct1-Bf_.js +1 -0
- package/dist/web/assets/cynefinDiagram-5FMLGOSQ-CuBJbnvx.js +62 -0
- package/dist/web/assets/cytoscape.esm-Yq6u8L66.js +321 -0
- package/dist/web/assets/dagre-C-rnJpZF.js +1 -0
- package/dist/web/assets/dagre-GXQ25YYZ-w8zs2moB.js +4 -0
- package/dist/web/assets/defaultLocale-BFoDCU3G.js +1 -0
- package/dist/web/assets/diagram-S7CK7UJ4-BMBhFewM.js +30 -0
- package/dist/web/assets/diagram-UQ7AKVKN-C7BN6534.js +41 -0
- package/dist/web/assets/diagram-VSXAHHWV-Dd8_wR6U.js +3 -0
- package/dist/web/assets/diagram-VX7I27RA-DR3aOC1b.js +24 -0
- package/dist/web/assets/diagram-Z3DM3KII-5aBBRZ2Z.js +24 -0
- package/dist/web/assets/diff-woXpYk--.js +1 -0
- package/dist/web/assets/dist-v5Q1xZ2K.js +1 -0
- package/dist/web/assets/ebnfDiagram-PWID7BFC-46Gc2APT.js +1 -0
- package/dist/web/assets/engine-CknQqOvU.js +44 -0
- package/dist/web/assets/erDiagram-RLTQ6QDP-CNfUFQmD.js +99 -0
- package/dist/web/assets/eventmodeling-NTZA5JFV-Dx_J4ait.js +1 -0
- package/dist/web/assets/flowDiagram-HODETNUW-Bs7QheBl.js +1 -0
- package/dist/web/assets/ganttDiagram-EL5Y4UJY-B1RLLE0F.js +292 -0
- package/dist/web/assets/gitGraph-4MIJSDKK-DmzdoCuA.js +1 -0
- package/dist/web/assets/gitGraphDiagram-WWUBYQGX-DN5-fMOS.js +106 -0
- package/dist/web/assets/github-dark-C-LZuMrd.js +1 -0
- package/dist/web/assets/github-light-EUqPIrTm.js +1 -0
- package/dist/web/assets/go-rLFTqkRN.js +1 -0
- package/dist/web/assets/graphlib-DS17s2tU.js +1 -0
- package/dist/web/assets/highlight-C1LfZ1aI.js +152 -0
- package/dist/web/assets/index-CGolUSWa.js +75 -0
- package/dist/web/assets/index-vUqwyH-2.css +1 -0
- package/dist/web/assets/info-A6RAGUB7-CebZRx1l.js +1 -0
- package/dist/web/assets/infoDiagram-27XIBGKW-BeY-irSX.js +2 -0
- package/dist/web/assets/init-C-OQMol4.js +1 -0
- package/dist/web/assets/ishikawaDiagram-5VMMS53U-Djn1dIqp.js +70 -0
- package/dist/web/assets/javascript-BgS3c2Ky.js +1 -0
- package/dist/web/assets/journeyDiagram-3NMN7TZE-CV_b2MnE.js +139 -0
- package/dist/web/assets/json-qhed-kSA.js +1 -0
- package/dist/web/assets/jsx-Bz0zcwM4.js +1 -0
- package/dist/web/assets/jsxRuntime-D9k5B_Bf.js +1 -0
- package/dist/web/assets/kanban-definition-UXKFOSKX-CXTukynL.js +89 -0
- package/dist/web/assets/katex-ZlcWpGUi.js +257 -0
- package/dist/web/assets/line-DIt63x6x.js +1 -0
- package/dist/web/assets/linear-SzVmGSw4.js +1 -0
- package/dist/web/assets/mermaid-parser.core-UsYAxtjN.js +7 -0
- package/dist/web/assets/mindmap-definition-YA3MSWOX-G0K_7S5N.js +96 -0
- package/dist/web/assets/ordinal-BDEzSJ7C.js +1 -0
- package/dist/web/assets/packet-AYTQ26CC-D3_L-O-_.js +1 -0
- package/dist/web/assets/path-fybaL0A-.js +1 -0
- package/dist/web/assets/pegDiagram-XKGWAZYB-BoSJtVsR.js +1 -0
- package/dist/web/assets/pie-WAS4IAKB-DUARjLYc.js +1 -0
- package/dist/web/assets/pieDiagram-E7YTZNPT-DaFXBymx.js +39 -0
- package/dist/web/assets/purify.es--06brOip.js +3 -0
- package/dist/web/assets/python-gzcpVVnB.js +1 -0
- package/dist/web/assets/quadrantDiagram-AXDQQJYC-DvJJGxS3.js +7 -0
- package/dist/web/assets/radar-RG4KPBEZ-CoTA2z0h.js +1 -0
- package/dist/web/assets/railroad-74A4TZTK-D22_YeTS.js +1 -0
- package/dist/web/assets/railroad-abnf-HS5TGJTU-Mvl9LvJX.js +1 -0
- package/dist/web/assets/railroad-ebnf-LZEXJU2U-hvVPkAs6.js +1 -0
- package/dist/web/assets/railroad-peg-WCYAUIDC-XX_5aYEK.js +1 -0
- package/dist/web/assets/railroadDiagram-O6MQD6OU-Cxnojv6Q.js +1 -0
- package/dist/web/assets/requirementDiagram-BXWQKSXE-gW2PlZJn.js +84 -0
- package/dist/web/assets/rolldown-runtime-hePW80VL.js +1 -0
- package/dist/web/assets/rough.esm-Dy-Kn_BL.js +1 -0
- package/dist/web/assets/rust-Cfkwpbl8.js +1 -0
- package/dist/web/assets/sankeyDiagram-P5KCCOFB-DcoJ0sYw.js +40 -0
- package/dist/web/assets/sequenceDiagram-WJ2MYXX4-kRLbkAfp.js +162 -0
- package/dist/web/assets/shellscript-CLZ0U2zV.js +1 -0
- package/dist/web/assets/sizeCapture-INFHLROL-3uHXrFqA.js +1 -0
- package/dist/web/assets/sql-BsFa4tDR.js +1 -0
- package/dist/web/assets/src-B6xuSHsQ.js +1 -0
- package/dist/web/assets/stateDiagram-D77RDMKH-CEWdwxTm.js +1 -0
- package/dist/web/assets/stateDiagram-v2-MP3YSRHH-DZ8dTBXf.js +1 -0
- package/dist/web/assets/swimlanes-42K2YHIH-oyoSxsbH.js +1 -0
- package/dist/web/assets/swimlanesDiagram-VR7AAH4N-McbEeiYz.js +8 -0
- package/dist/web/assets/timeline-definition-24CTP7MA-D97QYUq5.js +120 -0
- package/dist/web/assets/toml-CcmNWLt0.js +1 -0
- package/dist/web/assets/treeView-Q6P3EWNA-FwhRnXTt.js +1 -0
- package/dist/web/assets/treemap-WGGIJYW6-ByFNzzzU.js +1 -0
- package/dist/web/assets/tsx-udAQXfEw.js +1 -0
- package/dist/web/assets/typescript-bsJCZSQ-.js +1 -0
- package/dist/web/assets/vennDiagram-4TSXK5OY-CcqjaQy9.js +34 -0
- package/dist/web/assets/view-0zE8CdnP.js +1 -0
- package/dist/web/assets/view-BQZA99Bp.js +1 -0
- package/dist/web/assets/view-BqpAWkcP.css +1 -0
- package/dist/web/assets/view-C9vvQ0AK.js +1 -0
- package/dist/web/assets/view-CS-jPsy6.js +2 -0
- package/dist/web/assets/view-CYRKKlxP.js +1 -0
- package/dist/web/assets/view-CyO_m5dR.css +1 -0
- package/dist/web/assets/view-D7KCHoy1.css +1 -0
- package/dist/web/assets/view-DE2OI6Vv.js +5 -0
- package/dist/web/assets/view-DPqI4qBt.css +1 -0
- package/dist/web/assets/view-DWcZ1gKW.css +1 -0
- package/dist/web/assets/view-Dl8ihdis.css +1 -0
- package/dist/web/assets/view-DrJSW55A.css +1 -0
- package/dist/web/assets/view-DzlpughI.js +257 -0
- package/dist/web/assets/view-NDb0VvVU.js +6 -0
- package/dist/web/assets/wardley-WFR3VGLG-Bt4CePrl.js +1 -0
- package/dist/web/assets/wardleyDiagram-VM6X3IG4-CcgXimi3.js +78 -0
- package/dist/web/assets/xychartDiagram-S5SC5T6Z-0Jx20wNz.js +7 -0
- package/dist/web/assets/yaml-rwi0_p6S.js +1 -0
- package/dist/web/index.html +17 -0
- package/package.json +55 -0
package/dist/host/git.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-only git queries for the file routes (tree filtering, change marks, diffs). `spawn`
|
|
3
|
+
* without a shell, a 2 s timeout, bounded output, and options that keep git from running
|
|
4
|
+
* repository-configured programs (no fsmonitor, no external diff or textconv) or taking locks.
|
|
5
|
+
*/
|
|
6
|
+
import { spawn } from "node:child_process";
|
|
7
|
+
import { relative, resolve, sep } from "node:path";
|
|
8
|
+
const BASE = ["-c", "core.fsmonitor=false", "-c", "core.quotepath=off", "--no-optional-locks"];
|
|
9
|
+
/** Runs git in `cwd`; resolves with code -1 when git is missing, times out or overflows. */
|
|
10
|
+
export function runGit(cwd, args,
|
|
11
|
+
/** `literal`: pathspecs are plain paths (no globs or magic); unsupported by check-ignore. */
|
|
12
|
+
options = {}) {
|
|
13
|
+
const maxBytes = options.maxBytes ?? 8 * 1024 * 1024;
|
|
14
|
+
return new Promise((done) => {
|
|
15
|
+
let settled = false;
|
|
16
|
+
const finish = (result) => {
|
|
17
|
+
if (settled)
|
|
18
|
+
return;
|
|
19
|
+
settled = true;
|
|
20
|
+
clearTimeout(timer);
|
|
21
|
+
done(result);
|
|
22
|
+
};
|
|
23
|
+
let child;
|
|
24
|
+
try {
|
|
25
|
+
child = spawn("git", [...BASE, ...(options.literal ? ["--literal-pathspecs"] : []), ...args], {
|
|
26
|
+
cwd,
|
|
27
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
28
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: "0", GIT_OPTIONAL_LOCKS: "0" },
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
finish({ code: -1, stdout: "" });
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const timer = setTimeout(() => {
|
|
36
|
+
child.kill("SIGKILL");
|
|
37
|
+
finish({ code: -1, stdout: "" });
|
|
38
|
+
}, options.timeoutMs ?? 2_000);
|
|
39
|
+
const chunks = [];
|
|
40
|
+
let size = 0;
|
|
41
|
+
child.stdout?.on("data", (chunk) => {
|
|
42
|
+
size += chunk.length;
|
|
43
|
+
if (size > maxBytes) {
|
|
44
|
+
child.kill("SIGKILL");
|
|
45
|
+
finish({ code: -1, stdout: "" });
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
chunks.push(chunk);
|
|
49
|
+
});
|
|
50
|
+
child.on("error", () => finish({ code: -1, stdout: "" }));
|
|
51
|
+
child.on("close", (code) => finish({ code: code ?? -1, stdout: Buffer.concat(chunks).toString("utf8") }));
|
|
52
|
+
child.stdin?.on("error", () => { });
|
|
53
|
+
child.stdin?.end(options.input ?? "");
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
/** Top-level directory of the repository containing `cwd`, or undefined outside git. */
|
|
57
|
+
export async function gitRoot(cwd) {
|
|
58
|
+
const r = await runGit(cwd, ["rev-parse", "--show-toplevel"]);
|
|
59
|
+
return r.code === 0 && r.stdout.trim() ? r.stdout.trim() : undefined;
|
|
60
|
+
}
|
|
61
|
+
const toPosix = (path) => path.split(sep).join("/");
|
|
62
|
+
/**
|
|
63
|
+
* `git status --porcelain=v1 -z` of the repository as `workspace-relative path → code` (`M`,
|
|
64
|
+
* `A`, `D`, `R`, `??`…). Paths outside `workspace` (sibling directories of a monorepo) are left out.
|
|
65
|
+
*/
|
|
66
|
+
export async function gitStatus(workspace, top) {
|
|
67
|
+
const marks = new Map();
|
|
68
|
+
const r = await runGit(workspace, ["status", "--porcelain=v1", "-z", "--untracked-files=all"]);
|
|
69
|
+
if (r.code !== 0)
|
|
70
|
+
return marks;
|
|
71
|
+
const fields = r.stdout.split("\0");
|
|
72
|
+
for (let i = 0; i < fields.length; i++) {
|
|
73
|
+
const field = fields[i];
|
|
74
|
+
if (field.length < 4)
|
|
75
|
+
continue;
|
|
76
|
+
const xy = field.slice(0, 2);
|
|
77
|
+
const path = field.slice(3);
|
|
78
|
+
if (xy[0] === "R" || xy[0] === "C")
|
|
79
|
+
i++; // the next field is the original path
|
|
80
|
+
const code = xy === "??" ? "??" : ((xy[0] !== " " ? xy[0] : xy[1]) ?? "M");
|
|
81
|
+
const rel = relative(workspace, resolve(top, path));
|
|
82
|
+
if (rel.startsWith(".."))
|
|
83
|
+
continue;
|
|
84
|
+
marks.set(toPosix(rel), code);
|
|
85
|
+
}
|
|
86
|
+
return marks;
|
|
87
|
+
}
|
|
88
|
+
/** The subset of workspace-relative `paths` (directories end in `/`) that git ignores. */
|
|
89
|
+
export async function gitIgnored(workspace, paths) {
|
|
90
|
+
if (!paths.length)
|
|
91
|
+
return new Set();
|
|
92
|
+
const r = await runGit(workspace, ["check-ignore", "-z", "--stdin"], {
|
|
93
|
+
input: `${paths.join("\0")}\0`,
|
|
94
|
+
});
|
|
95
|
+
// 0: some ignored, 1: none, anything else: not a repository or an error (show everything).
|
|
96
|
+
if (r.code !== 0)
|
|
97
|
+
return new Set();
|
|
98
|
+
return new Set(r.stdout.split("\0").filter(Boolean));
|
|
99
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Policy } from "@alisio/core";
|
|
2
|
+
import type { PermissionPresetId, PermissionPresetInfo } from "@alisio/sdk";
|
|
3
|
+
export declare const PRESET_IDS: readonly PermissionPresetId[];
|
|
4
|
+
export declare const DEFAULT_PRESET: PermissionPresetId;
|
|
5
|
+
/**
|
|
6
|
+
* The capability ceiling of a workspace: the policy its app was launched with (the `alisio serve`
|
|
7
|
+
* flags) and whether approvals exist at all (not under `--read-only`).
|
|
8
|
+
*/
|
|
9
|
+
export interface Ceiling {
|
|
10
|
+
policy: Readonly<Policy>;
|
|
11
|
+
approvals: boolean;
|
|
12
|
+
readOnly: boolean;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* A preset narrowed by the ceiling: an effect the launch flags do not allow degrades to asking
|
|
16
|
+
* (the runner only ever narrows its own policy). Under `--read-only` only `read-only` exists.
|
|
17
|
+
*/
|
|
18
|
+
export declare function presetInfo(id: PermissionPresetId, ceiling: Ceiling): PermissionPresetInfo;
|
|
19
|
+
export declare const presetInfos: (ceiling: Ceiling) => PermissionPresetInfo[];
|
|
20
|
+
/** The stored preset of a session, falling back to the default (or `read-only` when forced). */
|
|
21
|
+
export declare function sessionPreset(options: Record<string, unknown> | undefined, ceiling: Ceiling): PermissionPresetId;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export const PRESET_IDS = [
|
|
2
|
+
"read-only",
|
|
3
|
+
"ask",
|
|
4
|
+
"workspace-write",
|
|
5
|
+
"full-access",
|
|
6
|
+
];
|
|
7
|
+
export const DEFAULT_PRESET = "workspace-write";
|
|
8
|
+
/** What each preset asks for before the server ceiling is applied (RF-08). */
|
|
9
|
+
const WANTED = {
|
|
10
|
+
"read-only": { policy: { write: false, process: false, external: false }, approvals: false },
|
|
11
|
+
ask: { policy: { write: false, process: false, external: false }, approvals: true },
|
|
12
|
+
"workspace-write": { policy: { write: true, process: false, external: false }, approvals: true },
|
|
13
|
+
"full-access": { policy: { write: true, process: true, external: true }, approvals: true },
|
|
14
|
+
};
|
|
15
|
+
const FLAG = {
|
|
16
|
+
write: "--allow-write",
|
|
17
|
+
process: "--allow-process",
|
|
18
|
+
external: "--allow-external",
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* A preset narrowed by the ceiling: an effect the launch flags do not allow degrades to asking
|
|
22
|
+
* (the runner only ever narrows its own policy). Under `--read-only` only `read-only` exists.
|
|
23
|
+
*/
|
|
24
|
+
export function presetInfo(id, ceiling) {
|
|
25
|
+
const wanted = WANTED[id];
|
|
26
|
+
const policy = {
|
|
27
|
+
write: wanted.policy.write && ceiling.policy.write,
|
|
28
|
+
process: wanted.policy.process && ceiling.policy.process,
|
|
29
|
+
external: wanted.policy.external && ceiling.policy.external,
|
|
30
|
+
};
|
|
31
|
+
if (ceiling.readOnly && id !== "read-only")
|
|
32
|
+
return {
|
|
33
|
+
id,
|
|
34
|
+
available: false,
|
|
35
|
+
reason: "The server was started with --read-only",
|
|
36
|
+
policy: { write: false, process: false, external: false },
|
|
37
|
+
approvals: false,
|
|
38
|
+
};
|
|
39
|
+
const degraded = Object.keys(FLAG).filter((effect) => wanted.policy[effect] && !ceiling.policy[effect]);
|
|
40
|
+
return {
|
|
41
|
+
id,
|
|
42
|
+
available: true,
|
|
43
|
+
...(degraded.length
|
|
44
|
+
? {
|
|
45
|
+
reason: `${degraded.join(", ")} still ask: the server was started without ${degraded.map((e) => FLAG[e]).join(", ")}`,
|
|
46
|
+
}
|
|
47
|
+
: {}),
|
|
48
|
+
policy,
|
|
49
|
+
approvals: wanted.approvals && ceiling.approvals,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export const presetInfos = (ceiling) => PRESET_IDS.map((id) => presetInfo(id, ceiling));
|
|
53
|
+
/** The stored preset of a session, falling back to the default (or `read-only` when forced). */
|
|
54
|
+
export function sessionPreset(options, ceiling) {
|
|
55
|
+
const stored = options?.preset;
|
|
56
|
+
const id = PRESET_IDS.includes(stored)
|
|
57
|
+
? stored
|
|
58
|
+
: DEFAULT_PRESET;
|
|
59
|
+
return presetInfo(id, ceiling).available ? id : "read-only";
|
|
60
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { Attachment } from "@alisio/sdk";
|
|
2
|
+
import type { Application } from "./workspace-host.ts";
|
|
3
|
+
type RunOptions = NonNullable<Parameters<Application["runner"]["run"]>[3]>;
|
|
4
|
+
/** One prompt accepted by the server: a `runs` row in `queued` until a slot is free. */
|
|
5
|
+
export interface RunJob {
|
|
6
|
+
runId: string;
|
|
7
|
+
sessionId: string;
|
|
8
|
+
workspaceId: string;
|
|
9
|
+
app: Application;
|
|
10
|
+
text: string;
|
|
11
|
+
correlationId: string;
|
|
12
|
+
/** Built when the run starts, so preset and effort changes while queued apply. */
|
|
13
|
+
options: () => Omit<RunOptions, "runId" | "correlationId" | "attachments">;
|
|
14
|
+
attachments?: Attachment[];
|
|
15
|
+
display?: string;
|
|
16
|
+
}
|
|
17
|
+
export interface RunSchedulerOptions {
|
|
18
|
+
/** Global semaphore across sessions and workspaces (RF-02, default 4). */
|
|
19
|
+
maxConcurrent?: number;
|
|
20
|
+
/** Called on every job state change (queued, running, finished). */
|
|
21
|
+
onChange?: (job: RunJob, status: "queued" | "running" | "finished") => void;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* FIFO scheduler with a global concurrency limit. A session has at most one job (queued or
|
|
25
|
+
* running); the runner itself enforces one active run per session.
|
|
26
|
+
*/
|
|
27
|
+
export declare class RunScheduler {
|
|
28
|
+
private options;
|
|
29
|
+
private jobs;
|
|
30
|
+
private queue;
|
|
31
|
+
private running;
|
|
32
|
+
private readonly max;
|
|
33
|
+
/** Compactions (or other exclusive work) per session, tracked for status and shutdown. */
|
|
34
|
+
private exclusive;
|
|
35
|
+
constructor(options?: RunSchedulerOptions);
|
|
36
|
+
/** Accepts a job; it starts now when a slot is free, otherwise it waits in FIFO order. */
|
|
37
|
+
submit(job: RunJob): "queued" | "running";
|
|
38
|
+
private pump;
|
|
39
|
+
private start;
|
|
40
|
+
/** The queued or running job of a session. */
|
|
41
|
+
job(sessionId: string): {
|
|
42
|
+
runId: string;
|
|
43
|
+
status: "queued" | "running";
|
|
44
|
+
} | undefined;
|
|
45
|
+
/** Whether a session has a job or exclusive work (compaction) in flight. */
|
|
46
|
+
busy(sessionId: string): boolean;
|
|
47
|
+
compacting(sessionId: string): boolean;
|
|
48
|
+
/** Whether any job or exclusive work of a workspace is in flight (blocks eviction). */
|
|
49
|
+
busyWorkspace(workspaceId: string): boolean;
|
|
50
|
+
/** Tracks exclusive per-session work (e.g. a manual compaction) until it settles. */
|
|
51
|
+
track(sessionId: string, workspaceId: string, work: Promise<unknown>, hooks?: {
|
|
52
|
+
abort?: () => void;
|
|
53
|
+
onDone?: () => void;
|
|
54
|
+
}): void;
|
|
55
|
+
/**
|
|
56
|
+
* Cancels the session's job: a queued one is removed and journaled `cancelled`; a running one
|
|
57
|
+
* is aborted (the runner journals it). `runId`, when given, must match.
|
|
58
|
+
*/
|
|
59
|
+
cancel(sessionId: string, runId?: string): boolean;
|
|
60
|
+
counts(): {
|
|
61
|
+
active: number;
|
|
62
|
+
queued: number;
|
|
63
|
+
};
|
|
64
|
+
/** Cancels everything and waits (bounded by `ms`) for running work to settle. */
|
|
65
|
+
shutdown(ms?: number): Promise<void>;
|
|
66
|
+
}
|
|
67
|
+
export {};
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FIFO scheduler with a global concurrency limit. A session has at most one job (queued or
|
|
3
|
+
* running); the runner itself enforces one active run per session.
|
|
4
|
+
*/
|
|
5
|
+
export class RunScheduler {
|
|
6
|
+
options;
|
|
7
|
+
jobs = new Map();
|
|
8
|
+
queue = [];
|
|
9
|
+
running = 0;
|
|
10
|
+
max;
|
|
11
|
+
/** Compactions (or other exclusive work) per session, tracked for status and shutdown. */
|
|
12
|
+
exclusive = new Map();
|
|
13
|
+
constructor(options = {}) {
|
|
14
|
+
this.options = options;
|
|
15
|
+
this.max = Math.max(1, options.maxConcurrent ?? 4);
|
|
16
|
+
}
|
|
17
|
+
/** Accepts a job; it starts now when a slot is free, otherwise it waits in FIFO order. */
|
|
18
|
+
submit(job) {
|
|
19
|
+
const tracked = { ...job, status: "queued", controller: new AbortController() };
|
|
20
|
+
this.jobs.set(job.sessionId, tracked);
|
|
21
|
+
this.queue.push(tracked);
|
|
22
|
+
this.options.onChange?.(job, "queued");
|
|
23
|
+
this.pump();
|
|
24
|
+
return tracked.status;
|
|
25
|
+
}
|
|
26
|
+
pump() {
|
|
27
|
+
while (this.running < this.max && this.queue.length) {
|
|
28
|
+
const job = this.queue.shift();
|
|
29
|
+
this.start(job);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
start(job) {
|
|
33
|
+
job.status = "running";
|
|
34
|
+
this.running++;
|
|
35
|
+
this.options.onChange?.(job, "running");
|
|
36
|
+
const { runner, store } = job.app;
|
|
37
|
+
let run;
|
|
38
|
+
try {
|
|
39
|
+
run = runner.run(job.sessionId, job.text, job.controller.signal, {
|
|
40
|
+
...job.options(),
|
|
41
|
+
runId: job.runId,
|
|
42
|
+
correlationId: job.correlationId,
|
|
43
|
+
...(job.attachments?.length ? { attachments: job.attachments } : {}),
|
|
44
|
+
...(job.display ? { display: job.display } : {}),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
run = Promise.reject(error);
|
|
49
|
+
}
|
|
50
|
+
job.done = run.then(() => undefined, (error) => {
|
|
51
|
+
// A run that never reached the runner's journal (e.g. the session was claimed) must not
|
|
52
|
+
// stay `queued`; terminal rows are never overwritten.
|
|
53
|
+
store.endRun?.(job.runId, {
|
|
54
|
+
status: job.controller.signal.aborted ? "cancelled" : "failed",
|
|
55
|
+
error: error instanceof Error ? error.message : String(error),
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
void job.done.finally(() => {
|
|
59
|
+
this.running--;
|
|
60
|
+
if (this.jobs.get(job.sessionId) === job)
|
|
61
|
+
this.jobs.delete(job.sessionId);
|
|
62
|
+
this.options.onChange?.(job, "finished");
|
|
63
|
+
this.pump();
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
/** The queued or running job of a session. */
|
|
67
|
+
job(sessionId) {
|
|
68
|
+
const job = this.jobs.get(sessionId);
|
|
69
|
+
return job ? { runId: job.runId, status: job.status } : undefined;
|
|
70
|
+
}
|
|
71
|
+
/** Whether a session has a job or exclusive work (compaction) in flight. */
|
|
72
|
+
busy(sessionId) {
|
|
73
|
+
return this.jobs.has(sessionId) || this.exclusive.has(sessionId);
|
|
74
|
+
}
|
|
75
|
+
compacting(sessionId) {
|
|
76
|
+
return this.exclusive.has(sessionId);
|
|
77
|
+
}
|
|
78
|
+
/** Whether any job or exclusive work of a workspace is in flight (blocks eviction). */
|
|
79
|
+
busyWorkspace(workspaceId) {
|
|
80
|
+
for (const job of this.jobs.values())
|
|
81
|
+
if (job.workspaceId === workspaceId)
|
|
82
|
+
return true;
|
|
83
|
+
for (const work of this.exclusive.values())
|
|
84
|
+
if (work.workspaceId === workspaceId)
|
|
85
|
+
return true;
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
/** Tracks exclusive per-session work (e.g. a manual compaction) until it settles. */
|
|
89
|
+
track(sessionId, workspaceId, work, hooks = {}) {
|
|
90
|
+
const done = work.catch(() => undefined);
|
|
91
|
+
const onDone = hooks.onDone;
|
|
92
|
+
this.exclusive.set(sessionId, {
|
|
93
|
+
workspaceId,
|
|
94
|
+
done,
|
|
95
|
+
...(hooks.abort ? { abort: hooks.abort } : {}),
|
|
96
|
+
});
|
|
97
|
+
void done.finally(() => {
|
|
98
|
+
if (this.exclusive.get(sessionId)?.done === done)
|
|
99
|
+
this.exclusive.delete(sessionId);
|
|
100
|
+
onDone?.();
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Cancels the session's job: a queued one is removed and journaled `cancelled`; a running one
|
|
105
|
+
* is aborted (the runner journals it). `runId`, when given, must match.
|
|
106
|
+
*/
|
|
107
|
+
cancel(sessionId, runId) {
|
|
108
|
+
const job = this.jobs.get(sessionId);
|
|
109
|
+
if (!job || (runId && job.runId !== runId))
|
|
110
|
+
return false;
|
|
111
|
+
job.controller.abort(new Error("Cancelled"));
|
|
112
|
+
if (job.status === "queued") {
|
|
113
|
+
this.queue = this.queue.filter((queued) => queued !== job);
|
|
114
|
+
this.jobs.delete(sessionId);
|
|
115
|
+
job.app.store.endRun?.(job.runId, { status: "cancelled", error: "Cancelled" });
|
|
116
|
+
this.options.onChange?.(job, "finished");
|
|
117
|
+
}
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
counts() {
|
|
121
|
+
return { active: this.running + this.exclusive.size, queued: this.queue.length };
|
|
122
|
+
}
|
|
123
|
+
/** Cancels everything and waits (bounded by `ms`) for running work to settle. */
|
|
124
|
+
async shutdown(ms = 3_000) {
|
|
125
|
+
for (const sessionId of [...this.jobs.keys()])
|
|
126
|
+
this.cancel(sessionId);
|
|
127
|
+
for (const work of this.exclusive.values())
|
|
128
|
+
work.abort?.();
|
|
129
|
+
const pending = [
|
|
130
|
+
...[...this.jobs.values()].flatMap((job) => (job.done ? [job.done] : [])),
|
|
131
|
+
...[...this.exclusive.values()].map((work) => work.done),
|
|
132
|
+
];
|
|
133
|
+
let timer;
|
|
134
|
+
await Promise.race([
|
|
135
|
+
Promise.allSettled(pending),
|
|
136
|
+
new Promise((resolve) => {
|
|
137
|
+
timer = setTimeout(resolve, ms);
|
|
138
|
+
}),
|
|
139
|
+
]);
|
|
140
|
+
if (timer)
|
|
141
|
+
clearTimeout(timer);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { type Policy, type Session, type SQLiteStore } from "@alisio/core";
|
|
2
|
+
import type { ServerFrame, SessionDetail, SessionDetailWire, SessionSummary, SessionUiStatus } from "@alisio/sdk";
|
|
3
|
+
import { type Ceiling } from "./presets.ts";
|
|
4
|
+
import type { RunScheduler } from "./run-scheduler.ts";
|
|
5
|
+
import { type OpenWorkspace, type ServerAppOptions, type WorkspaceHost } from "./workspace-host.ts";
|
|
6
|
+
export interface SessionServiceOptions {
|
|
7
|
+
catalog: SQLiteStore;
|
|
8
|
+
workspaces: WorkspaceHost;
|
|
9
|
+
scheduler: RunScheduler;
|
|
10
|
+
base: ServerAppOptions;
|
|
11
|
+
/** Whether an approval or interaction waits for input in a root session's tree. */
|
|
12
|
+
awaitingInput?: (rootSessionId: string) => boolean;
|
|
13
|
+
/** Sends a frame to every connected stream (session status for sidebars). */
|
|
14
|
+
broadcast?: (frame: ServerFrame) => void;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Session views (derived UI status §6.2, wire/summary/detail shapes) and per-session run
|
|
18
|
+
* policy. Reads go to the server's own store connection, so listing sessions never opens a
|
|
19
|
+
* workspace application.
|
|
20
|
+
*/
|
|
21
|
+
export declare class SessionService {
|
|
22
|
+
private options;
|
|
23
|
+
/** Per-session policy objects: "allow for session" widens only its own session. */
|
|
24
|
+
private policies;
|
|
25
|
+
readonly ceiling: Ceiling;
|
|
26
|
+
constructor(options: SessionServiceOptions);
|
|
27
|
+
/** A session by id, or 404. */
|
|
28
|
+
get(id: string): Session;
|
|
29
|
+
/** The root of a session (itself for roots). */
|
|
30
|
+
rootOf(id: string): string;
|
|
31
|
+
/** Derived UI status (§6.2); never persisted. */
|
|
32
|
+
status(session: Session): SessionUiStatus;
|
|
33
|
+
/**
|
|
34
|
+
* The stored title, else the first user prompt collapsed to one line and cut at 60
|
|
35
|
+
* characters (RF-10). Derived on read, never persisted, so a rename always wins.
|
|
36
|
+
*/
|
|
37
|
+
title(session: Session): string | undefined;
|
|
38
|
+
wire(session: Session): SessionDetailWire;
|
|
39
|
+
summary(session: Session): SessionSummary;
|
|
40
|
+
detail(session: Session): SessionDetail;
|
|
41
|
+
/**
|
|
42
|
+
* The policy object passed as `RunOptions.policy` for a session: the preset narrowed by the
|
|
43
|
+
* ceiling. The runner widens this object on "allow for session", so each session keeps its
|
|
44
|
+
* own object (the app-wide policy is never mutated by web approvals).
|
|
45
|
+
*/
|
|
46
|
+
policy(session: Session): {
|
|
47
|
+
policy: Policy;
|
|
48
|
+
approvals: boolean;
|
|
49
|
+
};
|
|
50
|
+
/** Drops session-scoped grants (after a preset change). */
|
|
51
|
+
resetPolicy(sessionId: string): void;
|
|
52
|
+
/**
|
|
53
|
+
* Run options for the session's next run: its own policy object, approvals, effort and the
|
|
54
|
+
* session's agent (stored id, else the app's active agent, as in the TUI). A read-only agent
|
|
55
|
+
* narrows the run to reads without approvals; its instructions are appended per run.
|
|
56
|
+
*/
|
|
57
|
+
runOptions(sessionId: string): {
|
|
58
|
+
policy: Policy;
|
|
59
|
+
approvals: boolean;
|
|
60
|
+
reasoningEffort?: string;
|
|
61
|
+
instructions?: string;
|
|
62
|
+
};
|
|
63
|
+
private agentOptions;
|
|
64
|
+
/** Merges keys into the session's stored options (undefined removes a key). */
|
|
65
|
+
setOptions(sessionId: string, options: Record<string, unknown>): void;
|
|
66
|
+
runByRequest(sessionId: string, requestId: string): import("@alisio/core").RunRecord | undefined;
|
|
67
|
+
beginRun(input: Parameters<SQLiteStore["beginRun"]>[0]): {
|
|
68
|
+
run: import("@alisio/core").RunRecord;
|
|
69
|
+
created: boolean;
|
|
70
|
+
};
|
|
71
|
+
/** Pid of another live process holding the session (for example the TUI). */
|
|
72
|
+
lockedBy(sessionId: string): number | undefined;
|
|
73
|
+
/** The already open workspace application of a session, without opening one. */
|
|
74
|
+
openApp(session: Session): OpenWorkspace | undefined;
|
|
75
|
+
/** The workspace application of a session, opened on demand. */
|
|
76
|
+
app(session: Session): Promise<OpenWorkspace>;
|
|
77
|
+
/** Broadcasts the current status of a session to every stream (sidebars). */
|
|
78
|
+
notify(sessionId: string): void;
|
|
79
|
+
/** Late binding for components created after this service. */
|
|
80
|
+
bind(hooks: Pick<SessionServiceOptions, "awaitingInput" | "broadcast">): void;
|
|
81
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { agentCatalogFromState, agentRunOptions, resolveActiveAgent, } from "@alisio/core";
|
|
2
|
+
import { HttpError } from "../http/errors.js";
|
|
3
|
+
import { presetInfos, sessionPreset } from "./presets.js";
|
|
4
|
+
import { workspaceId, } from "./workspace-host.js";
|
|
5
|
+
/**
|
|
6
|
+
* Session views (derived UI status §6.2, wire/summary/detail shapes) and per-session run
|
|
7
|
+
* policy. Reads go to the server's own store connection, so listing sessions never opens a
|
|
8
|
+
* workspace application.
|
|
9
|
+
*/
|
|
10
|
+
export class SessionService {
|
|
11
|
+
options;
|
|
12
|
+
/** Per-session policy objects: "allow for session" widens only its own session. */
|
|
13
|
+
policies = new Map();
|
|
14
|
+
ceiling;
|
|
15
|
+
constructor(options) {
|
|
16
|
+
this.options = options;
|
|
17
|
+
const base = options.base;
|
|
18
|
+
const readOnly = !!base.readOnly;
|
|
19
|
+
this.ceiling = {
|
|
20
|
+
policy: {
|
|
21
|
+
write: !readOnly && !!base.allowWrite,
|
|
22
|
+
process: !readOnly && !!base.allowProcess,
|
|
23
|
+
external: !readOnly && (!!base.allowExternal || !!base.allowMcp || !!base.allowAgents),
|
|
24
|
+
},
|
|
25
|
+
approvals: !readOnly,
|
|
26
|
+
readOnly,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/** A session by id, or 404. */
|
|
30
|
+
get(id) {
|
|
31
|
+
try {
|
|
32
|
+
return this.options.catalog.get(id);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
throw new HttpError("not_found", "Session not found");
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** The root of a session (itself for roots). */
|
|
39
|
+
rootOf(id) {
|
|
40
|
+
let current = id;
|
|
41
|
+
for (let depth = 0; depth < 32; depth++) {
|
|
42
|
+
let session;
|
|
43
|
+
try {
|
|
44
|
+
session = this.options.catalog.get(current);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return current;
|
|
48
|
+
}
|
|
49
|
+
if (!session.parentId)
|
|
50
|
+
return current;
|
|
51
|
+
current = session.parentId;
|
|
52
|
+
}
|
|
53
|
+
return current;
|
|
54
|
+
}
|
|
55
|
+
/** Derived UI status (§6.2); never persisted. */
|
|
56
|
+
status(session) {
|
|
57
|
+
const { catalog, scheduler } = this.options;
|
|
58
|
+
if (catalog.lockedBy(session.id))
|
|
59
|
+
return "locked";
|
|
60
|
+
const job = scheduler.job(session.id);
|
|
61
|
+
if (job?.status === "queued")
|
|
62
|
+
return "queued";
|
|
63
|
+
if (job || scheduler.compacting(session.id))
|
|
64
|
+
return this.options.awaitingInput?.(session.id) ? "awaiting_input" : "running";
|
|
65
|
+
const last = catalog.runs?.(session.id, { limit: 1 })[0];
|
|
66
|
+
return last && (last.status === "failed" || last.status === "interrupted") ? "error" : "idle";
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The stored title, else the first user prompt collapsed to one line and cut at 60
|
|
70
|
+
* characters (RF-10). Derived on read, never persisted, so a rename always wins.
|
|
71
|
+
*/
|
|
72
|
+
title(session) {
|
|
73
|
+
if (session.title)
|
|
74
|
+
return session.title;
|
|
75
|
+
const first = this.options.catalog
|
|
76
|
+
.messagesPage(session.id, { after: -1, limit: 4, compacted: true })
|
|
77
|
+
.items.find((i) => i.message.role === "user" && !i.message.summary)?.message;
|
|
78
|
+
if (first?.role !== "user")
|
|
79
|
+
return undefined;
|
|
80
|
+
const text = (first.display ?? first.text).replace(/\s+/g, " ").trim();
|
|
81
|
+
return text ? text.slice(0, 60) : undefined;
|
|
82
|
+
}
|
|
83
|
+
wire(session) {
|
|
84
|
+
const title = this.title(session);
|
|
85
|
+
return {
|
|
86
|
+
id: session.id,
|
|
87
|
+
workspaceId: workspaceId(session.workspace),
|
|
88
|
+
workspace: session.workspace,
|
|
89
|
+
provider: session.provider,
|
|
90
|
+
model: session.model,
|
|
91
|
+
status: session.parentId ? "idle" : this.status(session),
|
|
92
|
+
...(title ? { title } : {}),
|
|
93
|
+
...(session.parentId ? { parentId: session.parentId } : {}),
|
|
94
|
+
...(session.parentId && session.status ? { childStatus: session.status } : {}),
|
|
95
|
+
...(session.createdAt ? { createdAt: session.createdAt } : {}),
|
|
96
|
+
...(session.updatedAt ? { updatedAt: session.updatedAt } : {}),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
summary(session) {
|
|
100
|
+
return { ...this.wire(session), pinned: !!session.pinned, archived: !!session.archivedAt };
|
|
101
|
+
}
|
|
102
|
+
detail(session) {
|
|
103
|
+
const options = session.options ?? {};
|
|
104
|
+
return {
|
|
105
|
+
...this.summary(session),
|
|
106
|
+
preset: sessionPreset(options, this.ceiling),
|
|
107
|
+
...(typeof options.effort === "string" ? { effort: options.effort } : {}),
|
|
108
|
+
...(typeof options.agent === "string" ? { agent: options.agent } : {}),
|
|
109
|
+
presets: presetInfos(this.ceiling),
|
|
110
|
+
children: this.options.catalog.children(session.id).map((child) => this.wire(child)),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* The policy object passed as `RunOptions.policy` for a session: the preset narrowed by the
|
|
115
|
+
* ceiling. The runner widens this object on "allow for session", so each session keeps its
|
|
116
|
+
* own object (the app-wide policy is never mutated by web approvals).
|
|
117
|
+
*/
|
|
118
|
+
policy(session) {
|
|
119
|
+
const info = presetInfos(this.ceiling).find((p) => p.id === sessionPreset(session.options, this.ceiling));
|
|
120
|
+
let policy = this.policies.get(session.id);
|
|
121
|
+
if (!policy) {
|
|
122
|
+
policy = { ...(info?.policy ?? { write: false, process: false, external: false }) };
|
|
123
|
+
this.policies.set(session.id, policy);
|
|
124
|
+
}
|
|
125
|
+
return { policy, approvals: info?.approvals ?? false };
|
|
126
|
+
}
|
|
127
|
+
/** Drops session-scoped grants (after a preset change). */
|
|
128
|
+
resetPolicy(sessionId) {
|
|
129
|
+
this.policies.delete(sessionId);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Run options for the session's next run: its own policy object, approvals, effort and the
|
|
133
|
+
* session's agent (stored id, else the app's active agent, as in the TUI). A read-only agent
|
|
134
|
+
* narrows the run to reads without approvals; its instructions are appended per run.
|
|
135
|
+
*/
|
|
136
|
+
runOptions(sessionId) {
|
|
137
|
+
const session = this.get(sessionId);
|
|
138
|
+
const { policy, approvals } = this.policy(session);
|
|
139
|
+
const effort = session.options?.effort;
|
|
140
|
+
const agent = this.agentOptions(session);
|
|
141
|
+
return {
|
|
142
|
+
policy: agent.policy ? { ...agent.policy } : policy,
|
|
143
|
+
approvals: agent.approvals ?? approvals,
|
|
144
|
+
...(typeof effort === "string" ? { reasoningEffort: effort } : {}),
|
|
145
|
+
...(agent.instructions ? { instructions: agent.instructions } : {}),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
agentOptions(session) {
|
|
149
|
+
const app = this.openApp(session)?.app;
|
|
150
|
+
if (!app)
|
|
151
|
+
return {};
|
|
152
|
+
let state;
|
|
153
|
+
try {
|
|
154
|
+
state = app.plugins.pluginState("subagents", "mainAgents");
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
/* agent contributions are best-effort */
|
|
158
|
+
}
|
|
159
|
+
const stored = session.options?.agent;
|
|
160
|
+
const agent = resolveActiveAgent(agentCatalogFromState(state), typeof stored === "string" ? stored : app.config.agents.active);
|
|
161
|
+
return agentRunOptions(agent);
|
|
162
|
+
}
|
|
163
|
+
/** Merges keys into the session's stored options (undefined removes a key). */
|
|
164
|
+
setOptions(sessionId, options) {
|
|
165
|
+
this.options.catalog.updateSessionMeta(sessionId, { options });
|
|
166
|
+
}
|
|
167
|
+
runByRequest(sessionId, requestId) {
|
|
168
|
+
return this.options.catalog.runByRequest(sessionId, requestId);
|
|
169
|
+
}
|
|
170
|
+
beginRun(input) {
|
|
171
|
+
return this.options.catalog.beginRun(input);
|
|
172
|
+
}
|
|
173
|
+
/** Pid of another live process holding the session (for example the TUI). */
|
|
174
|
+
lockedBy(sessionId) {
|
|
175
|
+
return this.options.catalog.lockedBy(sessionId);
|
|
176
|
+
}
|
|
177
|
+
/** The already open workspace application of a session, without opening one. */
|
|
178
|
+
openApp(session) {
|
|
179
|
+
return this.options.workspaces.get(workspaceId(session.workspace));
|
|
180
|
+
}
|
|
181
|
+
/** The workspace application of a session, opened on demand. */
|
|
182
|
+
async app(session) {
|
|
183
|
+
const opened = await this.options.workspaces.openPath(session.workspace);
|
|
184
|
+
opened.lastUsed = Date.now();
|
|
185
|
+
return opened;
|
|
186
|
+
}
|
|
187
|
+
/** Broadcasts the current status of a session to every stream (sidebars). */
|
|
188
|
+
notify(sessionId) {
|
|
189
|
+
if (!this.options.broadcast)
|
|
190
|
+
return;
|
|
191
|
+
let session;
|
|
192
|
+
try {
|
|
193
|
+
session = this.options.catalog.get(sessionId);
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
this.options.broadcast({
|
|
199
|
+
t: "session_status",
|
|
200
|
+
sessionId,
|
|
201
|
+
workspaceId: workspaceId(session.workspace),
|
|
202
|
+
status: this.status(session),
|
|
203
|
+
...(this.title(session) ? { title: this.title(session) } : {}),
|
|
204
|
+
...(session.updatedAt ? { updatedAt: session.updatedAt } : {}),
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
/** Late binding for components created after this service. */
|
|
208
|
+
bind(hooks) {
|
|
209
|
+
Object.assign(this.options, hooks);
|
|
210
|
+
}
|
|
211
|
+
}
|