@marcoscale98/pi-extensible-workflows 5.14.1-fork.1 → 5.17.0-fork.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/CHANGELOG.md +67 -0
- package/dist/src/agent-execution.js +36 -51
- package/dist/src/background-widget.d.ts +56 -0
- package/dist/src/background-widget.js +22 -15
- package/dist/src/decoders.js +2 -1
- package/dist/src/host-navigator.d.ts +18 -0
- package/dist/src/host-navigator.js +71 -13
- package/dist/src/host-view.d.ts +16 -1
- package/dist/src/host-view.js +109 -42
- package/dist/src/host.js +57 -24
- package/dist/src/index.d.ts +3 -1
- package/dist/src/index.js +867 -545
- package/dist/src/index.js.map +3 -3
- package/dist/src/registry.js +2 -1
- package/dist/src/roles.d.ts +70 -0
- package/dist/src/roles.js +325 -0
- package/dist/src/settings.d.ts +13 -0
- package/dist/src/settings.js +253 -0
- package/dist/src/trajectory.d.ts +29 -4
- package/dist/src/trajectory.js +58 -3
- package/dist/src/types.d.ts +0 -1
- package/dist/src/utils.d.ts +2 -1
- package/dist/src/utils.js +10 -0
- package/dist/src/validation.d.ts +5 -17
- package/dist/src/validation.js +9 -458
- package/dist/subagents/index.js +1132 -694
- package/dist/subagents/index.js.map +3 -3
- package/dist/subagents/src/contracts.d.ts +4 -0
- package/dist/subagents/src/index.d.ts +1 -1
- package/dist/subagents/src/index.js +2 -1
- package/dist/subagents/src/manager.js +29 -10
- package/dist/subagents/src/navigator.js +396 -247
- package/dist/subagents/src/view.d.ts +23 -2
- package/dist/subagents/src/view.js +96 -30
- package/dist/trajectory/assets/index.html +347 -231
- package/dist/trajectory/assets/prism.min.js +13 -0
- package/dist/trajectory/index.js +377 -257
- package/dist/trajectory/index.js.map +3 -3
- package/dist/trajectory/src/assets/index.html +347 -231
- package/dist/trajectory/src/assets/prism.min.js +13 -0
- package/dist/trajectory/src/export.js +4 -2
- package/dist/trajectory/src/index.d.ts +2 -0
- package/dist/trajectory/src/index.js +143 -42
- package/dist/trajectory/src/server.js +134 -9
- package/dist/trajectory/src/server.js.map +1 -1
- package/package.json +2 -1
- package/src/agent-execution.ts +36 -42
- package/src/background-widget.ts +25 -18
- package/src/decoders.ts +2 -1
- package/src/host-navigator.ts +58 -14
- package/src/host-view.ts +93 -37
- package/src/host.ts +51 -21
- package/src/index.ts +3 -1
- package/src/registry.ts +2 -1
- package/src/roles.ts +326 -0
- package/src/settings.ts +206 -0
- package/src/trajectory.ts +76 -7
- package/src/types.ts +1 -1
- package/src/utils.ts +8 -1
- package/src/validation.ts +12 -356
- package/subagents/README.md +1 -1
- package/subagents/src/contracts.ts +4 -0
- package/subagents/src/index.ts +3 -2
- package/subagents/src/manager.ts +28 -10
- package/subagents/src/navigator.ts +336 -180
- package/subagents/src/view.ts +97 -28
- package/trajectory/src/assets/index.html +347 -231
- package/trajectory/src/assets/prism.min.js +13 -0
- package/trajectory/src/export.ts +4 -2
- package/trajectory/src/index.ts +118 -38
- package/trajectory/src/server.ts +112 -12
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prism v1.30.0 (core, clike, javascript, typescript, json, bash, diff, python) - https://prismjs.com
|
|
3
|
+
* Copyright (c) 2012 Lea Verou. (MIT Licensed)
|
|
4
|
+
*/
|
|
5
|
+
window.Prism = window.Prism || {}; window.Prism.manual = true;
|
|
6
|
+
var _self="undefined"!=typeof window?window:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{},Prism=function(e){var n=/(?:^|\s)lang(?:uage)?-([\w-]+)(?=\s|$)/i,t=0,r={},a={manual:e.Prism&&e.Prism.manual,disableWorkerMessageHandler:e.Prism&&e.Prism.disableWorkerMessageHandler,util:{encode:function e(n){return n instanceof i?new i(n.type,e(n.content),n.alias):Array.isArray(n)?n.map(e):n.replace(/&/g,"&").replace(/</g,"<").replace(/\u00a0/g," ")},type:function(e){return Object.prototype.toString.call(e).slice(8,-1)},objId:function(e){return e.__id||Object.defineProperty(e,"__id",{value:++t}),e.__id},clone:function e(n,t){var r,i;switch(t=t||{},a.util.type(n)){case"Object":if(i=a.util.objId(n),t[i])return t[i];for(var l in r={},t[i]=r,n)n.hasOwnProperty(l)&&(r[l]=e(n[l],t));return r;case"Array":return i=a.util.objId(n),t[i]?t[i]:(r=[],t[i]=r,n.forEach((function(n,a){r[a]=e(n,t)})),r);default:return n}},getLanguage:function(e){for(;e;){var t=n.exec(e.className);if(t)return t[1].toLowerCase();e=e.parentElement}return"none"},setLanguage:function(e,t){e.className=e.className.replace(RegExp(n,"gi"),""),e.classList.add("language-"+t)},currentScript:function(){if("undefined"==typeof document)return null;if(document.currentScript&&"SCRIPT"===document.currentScript.tagName)return document.currentScript;try{throw new Error}catch(r){var e=(/at [^(\r\n]*\((.*):[^:]+:[^:]+\)$/i.exec(r.stack)||[])[1];if(e){var n=document.getElementsByTagName("script");for(var t in n)if(n[t].src==e)return n[t]}return null}},isActive:function(e,n,t){for(var r="no-"+n;e;){var a=e.classList;if(a.contains(n))return!0;if(a.contains(r))return!1;e=e.parentElement}return!!t}},languages:{plain:r,plaintext:r,text:r,txt:r,extend:function(e,n){var t=a.util.clone(a.languages[e]);for(var r in n)t[r]=n[r];return t},insertBefore:function(e,n,t,r){var i=(r=r||a.languages)[e],l={};for(var o in i)if(i.hasOwnProperty(o)){if(o==n)for(var s in t)t.hasOwnProperty(s)&&(l[s]=t[s]);t.hasOwnProperty(o)||(l[o]=i[o])}var u=r[e];return r[e]=l,a.languages.DFS(a.languages,(function(n,t){t===u&&n!=e&&(this[n]=l)})),l},DFS:function e(n,t,r,i){i=i||{};var l=a.util.objId;for(var o in n)if(n.hasOwnProperty(o)){t.call(n,o,n[o],r||o);var s=n[o],u=a.util.type(s);"Object"!==u||i[l(s)]?"Array"!==u||i[l(s)]||(i[l(s)]=!0,e(s,t,o,i)):(i[l(s)]=!0,e(s,t,null,i))}}},plugins:{},highlightAll:function(e,n){a.highlightAllUnder(document,e,n)},highlightAllUnder:function(e,n,t){var r={callback:t,container:e,selector:'code[class*="language-"], [class*="language-"] code, code[class*="lang-"], [class*="lang-"] code'};a.hooks.run("before-highlightall",r),r.elements=Array.prototype.slice.apply(r.container.querySelectorAll(r.selector)),a.hooks.run("before-all-elements-highlight",r);for(var i,l=0;i=r.elements[l++];)a.highlightElement(i,!0===n,r.callback)},highlightElement:function(n,t,r){var i=a.util.getLanguage(n),l=a.languages[i];a.util.setLanguage(n,i);var o=n.parentElement;o&&"pre"===o.nodeName.toLowerCase()&&a.util.setLanguage(o,i);var s={element:n,language:i,grammar:l,code:n.textContent};function u(e){s.highlightedCode=e,a.hooks.run("before-insert",s),s.element.innerHTML=s.highlightedCode,a.hooks.run("after-highlight",s),a.hooks.run("complete",s),r&&r.call(s.element)}if(a.hooks.run("before-sanity-check",s),(o=s.element.parentElement)&&"pre"===o.nodeName.toLowerCase()&&!o.hasAttribute("tabindex")&&o.setAttribute("tabindex","0"),!s.code)return a.hooks.run("complete",s),void(r&&r.call(s.element));if(a.hooks.run("before-highlight",s),s.grammar)if(t&&e.Worker){var c=new Worker(a.filename);c.onmessage=function(e){u(e.data)},c.postMessage(JSON.stringify({language:s.language,code:s.code,immediateClose:!0}))}else u(a.highlight(s.code,s.grammar,s.language));else u(a.util.encode(s.code))},highlight:function(e,n,t){var r={code:e,grammar:n,language:t};if(a.hooks.run("before-tokenize",r),!r.grammar)throw new Error('The language "'+r.language+'" has no grammar.');return r.tokens=a.tokenize(r.code,r.grammar),a.hooks.run("after-tokenize",r),i.stringify(a.util.encode(r.tokens),r.language)},tokenize:function(e,n){var t=n.rest;if(t){for(var r in t)n[r]=t[r];delete n.rest}var a=new s;return u(a,a.head,e),o(e,a,n,a.head,0),function(e){for(var n=[],t=e.head.next;t!==e.tail;)n.push(t.value),t=t.next;return n}(a)},hooks:{all:{},add:function(e,n){var t=a.hooks.all;t[e]=t[e]||[],t[e].push(n)},run:function(e,n){var t=a.hooks.all[e];if(t&&t.length)for(var r,i=0;r=t[i++];)r(n)}},Token:i};function i(e,n,t,r){this.type=e,this.content=n,this.alias=t,this.length=0|(r||"").length}function l(e,n,t,r){e.lastIndex=n;var a=e.exec(t);if(a&&r&&a[1]){var i=a[1].length;a.index+=i,a[0]=a[0].slice(i)}return a}function o(e,n,t,r,s,g){for(var f in t)if(t.hasOwnProperty(f)&&t[f]){var h=t[f];h=Array.isArray(h)?h:[h];for(var d=0;d<h.length;++d){if(g&&g.cause==f+","+d)return;var v=h[d],p=v.inside,m=!!v.lookbehind,y=!!v.greedy,k=v.alias;if(y&&!v.pattern.global){var x=v.pattern.toString().match(/[imsuy]*$/)[0];v.pattern=RegExp(v.pattern.source,x+"g")}for(var b=v.pattern||v,w=r.next,A=s;w!==n.tail&&!(g&&A>=g.reach);A+=w.value.length,w=w.next){var P=w.value;if(n.length>e.length)return;if(!(P instanceof i)){var E,S=1;if(y){if(!(E=l(b,A,e,m))||E.index>=e.length)break;var L=E.index,O=E.index+E[0].length,C=A;for(C+=w.value.length;L>=C;)C+=(w=w.next).value.length;if(A=C-=w.value.length,w.value instanceof i)continue;for(var j=w;j!==n.tail&&(C<O||"string"==typeof j.value);j=j.next)S++,C+=j.value.length;S--,P=e.slice(A,C),E.index-=A}else if(!(E=l(b,0,P,m)))continue;L=E.index;var N=E[0],_=P.slice(0,L),M=P.slice(L+N.length),W=A+P.length;g&&W>g.reach&&(g.reach=W);var I=w.prev;if(_&&(I=u(n,I,_),A+=_.length),c(n,I,S),w=u(n,I,new i(f,p?a.tokenize(N,p):N,k,N)),M&&u(n,w,M),S>1){var T={cause:f+","+d,reach:W};o(e,n,t,w.prev,A,T),g&&T.reach>g.reach&&(g.reach=T.reach)}}}}}}function s(){var e={value:null,prev:null,next:null},n={value:null,prev:e,next:null};e.next=n,this.head=e,this.tail=n,this.length=0}function u(e,n,t){var r=n.next,a={value:t,prev:n,next:r};return n.next=a,r.prev=a,e.length++,a}function c(e,n,t){for(var r=n.next,a=0;a<t&&r!==e.tail;a++)r=r.next;n.next=r,r.prev=n,e.length-=a}if(e.Prism=a,i.stringify=function e(n,t){if("string"==typeof n)return n;if(Array.isArray(n)){var r="";return n.forEach((function(n){r+=e(n,t)})),r}var i={type:n.type,content:e(n.content,t),tag:"span",classes:["token",n.type],attributes:{},language:t},l=n.alias;l&&(Array.isArray(l)?Array.prototype.push.apply(i.classes,l):i.classes.push(l)),a.hooks.run("wrap",i);var o="";for(var s in i.attributes)o+=" "+s+'="'+(i.attributes[s]||"").replace(/"/g,""")+'"';return"<"+i.tag+' class="'+i.classes.join(" ")+'"'+o+">"+i.content+"</"+i.tag+">"},!e.document)return e.addEventListener?(a.disableWorkerMessageHandler||e.addEventListener("message",(function(n){var t=JSON.parse(n.data),r=t.language,i=t.code,l=t.immediateClose;e.postMessage(a.highlight(i,a.languages[r],r)),l&&e.close()}),!1),a):a;var g=a.util.currentScript();function f(){a.manual||a.highlightAll()}if(g&&(a.filename=g.src,g.hasAttribute("data-manual")&&(a.manual=!0)),!a.manual){var h=document.readyState;"loading"===h||"interactive"===h&&g&&g.defer?document.addEventListener("DOMContentLoaded",f):window.requestAnimationFrame?window.requestAnimationFrame(f):window.setTimeout(f,16)}return a}(_self);"undefined"!=typeof module&&module.exports&&(module.exports=Prism),"undefined"!=typeof global&&(global.Prism=Prism);
|
|
7
|
+
Prism.languages.clike={comment:[{pattern:/(^|[^\\])\/\*[\s\S]*?(?:\*\/|$)/,lookbehind:!0,greedy:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0,greedy:!0}],string:{pattern:/(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0},"class-name":{pattern:/(\b(?:class|extends|implements|instanceof|interface|new|trait)\s+|\bcatch\s+\()[\w.\\]+/i,lookbehind:!0,inside:{punctuation:/[.\\]/}},keyword:/\b(?:break|catch|continue|do|else|finally|for|function|if|in|instanceof|new|null|return|throw|try|while)\b/,boolean:/\b(?:false|true)\b/,function:/\b\w+(?=\()/,number:/\b0x[\da-f]+\b|(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:e[+-]?\d+)?/i,operator:/[<>]=?|[!=]=?=?|--?|\+\+?|&&?|\|\|?|[?*/~^%]/,punctuation:/[{}[\];(),.:]/};
|
|
8
|
+
Prism.languages.javascript=Prism.languages.extend("clike",{"class-name":[Prism.languages.clike["class-name"],{pattern:/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$A-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\.(?:constructor|prototype))/,lookbehind:!0}],keyword:[{pattern:/((?:^|\})\s*)catch\b/,lookbehind:!0},{pattern:/(^|[^.]|\.\.\.\s*)\b(?:as|assert(?=\s*\{)|async(?=\s*(?:function\b|\(|[$\w\xA0-\uFFFF]|$))|await|break|case|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally(?=\s*(?:\{|$))|for|from(?=\s*(?:['"]|$))|function|(?:get|set)(?=\s*(?:[#\[$\w\xA0-\uFFFF]|$))|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)\b/,lookbehind:!0}],function:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*(?:\.\s*(?:apply|bind|call)\s*)?\()/,number:{pattern:RegExp("(^|[^\\w$])(?:NaN|Infinity|0[bB][01]+(?:_[01]+)*n?|0[oO][0-7]+(?:_[0-7]+)*n?|0[xX][\\dA-Fa-f]+(?:_[\\dA-Fa-f]+)*n?|\\d+(?:_\\d+)*n|(?:\\d+(?:_\\d+)*(?:\\.(?:\\d+(?:_\\d+)*)?)?|\\.\\d+(?:_\\d+)*)(?:[Ee][+-]?\\d+(?:_\\d+)*)?)(?![\\w$])"),lookbehind:!0},operator:/--|\+\+|\*\*=?|=>|&&=?|\|\|=?|[!=]==|<<=?|>>>?=?|[-+*/%&|^!=<>]=?|\.{3}|\?\?=?|\?\.?|[~:]/}),Prism.languages.javascript["class-name"][0].pattern=/(\b(?:class|extends|implements|instanceof|interface|new)\s+)[\w.\\]+/,Prism.languages.insertBefore("javascript","keyword",{regex:{pattern:RegExp("((?:^|[^$\\w\\xA0-\\uFFFF.\"'\\])\\s]|\\b(?:return|yield))\\s*)/(?:(?:\\[(?:[^\\]\\\\\r\n]|\\\\.)*\\]|\\\\.|[^/\\\\\\[\r\n])+/[dgimyus]{0,7}|(?:\\[(?:[^[\\]\\\\\r\n]|\\\\.|\\[(?:[^[\\]\\\\\r\n]|\\\\.|\\[(?:[^[\\]\\\\\r\n]|\\\\.)*\\])*\\])*\\]|\\\\.|[^/\\\\\\[\r\n])+/[dgimyus]{0,7}v[dgimyus]{0,7})(?=(?:\\s|/\\*(?:[^*]|\\*(?!/))*\\*/)*(?:$|[\r\n,.;:})\\]]|//))"),lookbehind:!0,greedy:!0,inside:{"regex-source":{pattern:/^(\/)[\s\S]+(?=\/[a-z]*$)/,lookbehind:!0,alias:"language-regex",inside:Prism.languages.regex},"regex-delimiter":/^\/|\/$/,"regex-flags":/^[a-z]+$/}},"function-variable":{pattern:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*[=:]\s*(?:async\s*)?(?:\bfunction\b|(?:\((?:[^()]|\([^()]*\))*\)|(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)\s*=>))/,alias:"function"},parameter:[{pattern:/(function(?:\s+(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)?\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\))/,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$a-z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*=>)/i,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/(\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*=>)/,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/((?:\b|\s|^)(?!(?:as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)(?![$\w\xA0-\uFFFF]))(?:(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*\s*)\(\s*|\]\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*\{)/,lookbehind:!0,inside:Prism.languages.javascript}],constant:/\b[A-Z](?:[A-Z_]|\dx?)*\b/}),Prism.languages.insertBefore("javascript","string",{hashbang:{pattern:/^#!.*/,greedy:!0,alias:"comment"},"template-string":{pattern:/`(?:\\[\s\S]|\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}|(?!\$\{)[^\\`])*`/,greedy:!0,inside:{"template-punctuation":{pattern:/^`|`$/,alias:"string"},interpolation:{pattern:/((?:^|[^\\])(?:\\{2})*)\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}/,lookbehind:!0,inside:{"interpolation-punctuation":{pattern:/^\$\{|\}$/,alias:"punctuation"},rest:Prism.languages.javascript}},string:/[\s\S]+/}},"string-property":{pattern:/((?:^|[,{])[ \t]*)(["'])(?:\\(?:\r\n|[\s\S])|(?!\2)[^\\\r\n])*\2(?=\s*:)/m,lookbehind:!0,greedy:!0,alias:"property"}}),Prism.languages.insertBefore("javascript","operator",{"literal-property":{pattern:/((?:^|[,{])[ \t]*)(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*:)/m,lookbehind:!0,alias:"property"}}),Prism.languages.markup&&(Prism.languages.markup.tag.addInlined("script","javascript"),Prism.languages.markup.tag.addAttribute("on(?:abort|blur|change|click|composition(?:end|start|update)|dblclick|error|focus(?:in|out)?|key(?:down|up)|load|mouse(?:down|enter|leave|move|out|over|up)|reset|resize|scroll|select|slotchange|submit|unload|wheel)","javascript")),Prism.languages.js=Prism.languages.javascript;
|
|
9
|
+
!function(e){e.languages.typescript=e.languages.extend("javascript",{"class-name":{pattern:/(\b(?:class|extends|implements|instanceof|interface|new|type)\s+)(?!keyof\b)(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?:\s*<(?:[^<>]|<(?:[^<>]|<[^<>]*>)*>)*>)?/,lookbehind:!0,greedy:!0,inside:null},builtin:/\b(?:Array|Function|Promise|any|boolean|console|never|number|string|symbol|unknown)\b/}),e.languages.typescript.keyword.push(/\b(?:abstract|declare|is|keyof|readonly|require)\b/,/\b(?:asserts|infer|interface|module|namespace|type)\b(?=\s*(?:[{_$a-zA-Z\xA0-\uFFFF]|$))/,/\btype\b(?=\s*(?:[\{*]|$))/),delete e.languages.typescript.parameter,delete e.languages.typescript["literal-property"];var s=e.languages.extend("typescript",{});delete s["class-name"],e.languages.typescript["class-name"].inside=s,e.languages.insertBefore("typescript","function",{decorator:{pattern:/@[$\w\xA0-\uFFFF]+/,inside:{at:{pattern:/^@/,alias:"operator"},function:/^[\s\S]+/}},"generic-function":{pattern:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*\s*<(?:[^<>]|<(?:[^<>]|<[^<>]*>)*>)*>(?=\s*\()/,greedy:!0,inside:{function:/^#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*/,generic:{pattern:/<[\s\S]+/,alias:"class-name",inside:s}}}}),e.languages.ts=e.languages.typescript}(Prism);
|
|
10
|
+
Prism.languages.json={property:{pattern:/(^|[^\\])"(?:\\.|[^\\"\r\n])*"(?=\s*:)/,lookbehind:!0,greedy:!0},string:{pattern:/(^|[^\\])"(?:\\.|[^\\"\r\n])*"(?!\s*:)/,lookbehind:!0,greedy:!0},comment:{pattern:/\/\/.*|\/\*[\s\S]*?(?:\*\/|$)/,greedy:!0},number:/-?\b\d+(?:\.\d+)?(?:e[+-]?\d+)?\b/i,punctuation:/[{}[\],]/,operator:/:/,boolean:/\b(?:false|true)\b/,null:{pattern:/\bnull\b/,alias:"keyword"}},Prism.languages.webmanifest=Prism.languages.json;
|
|
11
|
+
!function(e){var t="\\b(?:BASH|BASHOPTS|BASH_ALIASES|BASH_ARGC|BASH_ARGV|BASH_CMDS|BASH_COMPLETION_COMPAT_DIR|BASH_LINENO|BASH_REMATCH|BASH_SOURCE|BASH_VERSINFO|BASH_VERSION|COLORTERM|COLUMNS|COMP_WORDBREAKS|DBUS_SESSION_BUS_ADDRESS|DEFAULTS_PATH|DESKTOP_SESSION|DIRSTACK|DISPLAY|EUID|GDMSESSION|GDM_LANG|GNOME_KEYRING_CONTROL|GNOME_KEYRING_PID|GPG_AGENT_INFO|GROUPS|HISTCONTROL|HISTFILE|HISTFILESIZE|HISTSIZE|HOME|HOSTNAME|HOSTTYPE|IFS|INSTANCE|JOB|LANG|LANGUAGE|LC_ADDRESS|LC_ALL|LC_IDENTIFICATION|LC_MEASUREMENT|LC_MONETARY|LC_NAME|LC_NUMERIC|LC_PAPER|LC_TELEPHONE|LC_TIME|LESSCLOSE|LESSOPEN|LINES|LOGNAME|LS_COLORS|MACHTYPE|MAILCHECK|MANDATORY_PATH|NO_AT_BRIDGE|OLDPWD|OPTERR|OPTIND|ORBIT_SOCKETDIR|OSTYPE|PAPERSIZE|PATH|PIPESTATUS|PPID|PS1|PS2|PS3|PS4|PWD|RANDOM|REPLY|SECONDS|SELINUX_INIT|SESSION|SESSIONTYPE|SESSION_MANAGER|SHELL|SHELLOPTS|SHLVL|SSH_AUTH_SOCK|TERM|UID|UPSTART_EVENTS|UPSTART_INSTANCE|UPSTART_JOB|UPSTART_SESSION|USER|WINDOWID|XAUTHORITY|XDG_CONFIG_DIRS|XDG_CURRENT_DESKTOP|XDG_DATA_DIRS|XDG_GREETER_DATA_DIR|XDG_MENU_PREFIX|XDG_RUNTIME_DIR|XDG_SEAT|XDG_SEAT_PATH|XDG_SESSION_DESKTOP|XDG_SESSION_ID|XDG_SESSION_PATH|XDG_SESSION_TYPE|XDG_VTNR|XMODIFIERS)\\b",a={pattern:/(^(["']?)\w+\2)[ \t]+\S.*/,lookbehind:!0,alias:"punctuation",inside:null},n={bash:a,environment:{pattern:RegExp("\\$"+t),alias:"constant"},variable:[{pattern:/\$?\(\([\s\S]+?\)\)/,greedy:!0,inside:{variable:[{pattern:/(^\$\(\([\s\S]+)\)\)/,lookbehind:!0},/^\$\(\(/],number:/\b0x[\dA-Fa-f]+\b|(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:[Ee]-?\d+)?/,operator:/--|\+\+|\*\*=?|<<=?|>>=?|&&|\|\||[=!+\-*/%<>^&|]=?|[?~:]/,punctuation:/\(\(?|\)\)?|,|;/}},{pattern:/\$\((?:\([^)]+\)|[^()])+\)|`[^`]+`/,greedy:!0,inside:{variable:/^\$\(|^`|\)$|`$/}},{pattern:/\$\{[^}]+\}/,greedy:!0,inside:{operator:/:[-=?+]?|[!\/]|##?|%%?|\^\^?|,,?/,punctuation:/[\[\]]/,environment:{pattern:RegExp("(\\{)"+t),lookbehind:!0,alias:"constant"}}},/\$(?:\w+|[#?*!@$])/],entity:/\\(?:[abceEfnrtv\\"]|O?[0-7]{1,3}|U[0-9a-fA-F]{8}|u[0-9a-fA-F]{4}|x[0-9a-fA-F]{1,2})/};e.languages.bash={shebang:{pattern:/^#!\s*\/.*/,alias:"important"},comment:{pattern:/(^|[^"{\\$])#.*/,lookbehind:!0},"function-name":[{pattern:/(\bfunction\s+)[\w-]+(?=(?:\s*\(?:\s*\))?\s*\{)/,lookbehind:!0,alias:"function"},{pattern:/\b[\w-]+(?=\s*\(\s*\)\s*\{)/,alias:"function"}],"for-or-select":{pattern:/(\b(?:for|select)\s+)\w+(?=\s+in\s)/,alias:"variable",lookbehind:!0},"assign-left":{pattern:/(^|[\s;|&]|[<>]\()\w+(?:\.\w+)*(?=\+?=)/,inside:{environment:{pattern:RegExp("(^|[\\s;|&]|[<>]\\()"+t),lookbehind:!0,alias:"constant"}},alias:"variable",lookbehind:!0},parameter:{pattern:/(^|\s)-{1,2}(?:\w+:[+-]?)?\w+(?:\.\w+)*(?=[=\s]|$)/,alias:"variable",lookbehind:!0},string:[{pattern:/((?:^|[^<])<<-?\s*)(\w+)\s[\s\S]*?(?:\r?\n|\r)\2/,lookbehind:!0,greedy:!0,inside:n},{pattern:/((?:^|[^<])<<-?\s*)(["'])(\w+)\2\s[\s\S]*?(?:\r?\n|\r)\3/,lookbehind:!0,greedy:!0,inside:{bash:a}},{pattern:/(^|[^\\](?:\\\\)*)"(?:\\[\s\S]|\$\([^)]+\)|\$(?!\()|`[^`]+`|[^"\\`$])*"/,lookbehind:!0,greedy:!0,inside:n},{pattern:/(^|[^$\\])'[^']*'/,lookbehind:!0,greedy:!0},{pattern:/\$'(?:[^'\\]|\\[\s\S])*'/,greedy:!0,inside:{entity:n.entity}}],environment:{pattern:RegExp("\\$?"+t),alias:"constant"},variable:n.variable,function:{pattern:/(^|[\s;|&]|[<>]\()(?:add|apropos|apt|apt-cache|apt-get|aptitude|aspell|automysqlbackup|awk|basename|bash|bc|bconsole|bg|bzip2|cal|cargo|cat|cfdisk|chgrp|chkconfig|chmod|chown|chroot|cksum|clear|cmp|column|comm|composer|cp|cron|crontab|csplit|curl|cut|date|dc|dd|ddrescue|debootstrap|df|diff|diff3|dig|dir|dircolors|dirname|dirs|dmesg|docker|docker-compose|du|egrep|eject|env|ethtool|expand|expect|expr|fdformat|fdisk|fg|fgrep|file|find|fmt|fold|format|free|fsck|ftp|fuser|gawk|git|gparted|grep|groupadd|groupdel|groupmod|groups|grub-mkconfig|gzip|halt|head|hg|history|host|hostname|htop|iconv|id|ifconfig|ifdown|ifup|import|install|ip|java|jobs|join|kill|killall|less|link|ln|locate|logname|logrotate|look|lpc|lpr|lprint|lprintd|lprintq|lprm|ls|lsof|lynx|make|man|mc|mdadm|mkconfig|mkdir|mke2fs|mkfifo|mkfs|mkisofs|mknod|mkswap|mmv|more|most|mount|mtools|mtr|mutt|mv|nano|nc|netstat|nice|nl|node|nohup|notify-send|npm|nslookup|op|open|parted|passwd|paste|pathchk|ping|pkill|pnpm|podman|podman-compose|popd|pr|printcap|printenv|ps|pushd|pv|quota|quotacheck|quotactl|ram|rar|rcp|reboot|remsync|rename|renice|rev|rm|rmdir|rpm|rsync|scp|screen|sdiff|sed|sendmail|seq|service|sftp|sh|shellcheck|shuf|shutdown|sleep|slocate|sort|split|ssh|stat|strace|su|sudo|sum|suspend|swapon|sync|sysctl|tac|tail|tar|tee|time|timeout|top|touch|tr|traceroute|tsort|tty|umount|uname|unexpand|uniq|units|unrar|unshar|unzip|update-grub|uptime|useradd|userdel|usermod|users|uudecode|uuencode|v|vcpkg|vdir|vi|vim|virsh|vmstat|wait|watch|wc|wget|whereis|which|who|whoami|write|xargs|xdg-open|yarn|yes|zenity|zip|zsh|zypper)(?=$|[)\s;|&])/,lookbehind:!0},keyword:{pattern:/(^|[\s;|&]|[<>]\()(?:case|do|done|elif|else|esac|fi|for|function|if|in|select|then|until|while)(?=$|[)\s;|&])/,lookbehind:!0},builtin:{pattern:/(^|[\s;|&]|[<>]\()(?:\.|:|alias|bind|break|builtin|caller|cd|command|continue|declare|echo|enable|eval|exec|exit|export|getopts|hash|help|let|local|logout|mapfile|printf|pwd|read|readarray|readonly|return|set|shift|shopt|source|test|times|trap|type|typeset|ulimit|umask|unalias|unset)(?=$|[)\s;|&])/,lookbehind:!0,alias:"class-name"},boolean:{pattern:/(^|[\s;|&]|[<>]\()(?:false|true)(?=$|[)\s;|&])/,lookbehind:!0},"file-descriptor":{pattern:/\B&\d\b/,alias:"important"},operator:{pattern:/\d?<>|>\||\+=|=[=~]?|!=?|<<[<-]?|[&\d]?>>|\d[<>]&?|[<>][&=]?|&[>&]?|\|[&|]?/,inside:{"file-descriptor":{pattern:/^\d/,alias:"important"}}},punctuation:/\$?\(\(?|\)\)?|\.\.|[{}[\];\\]/,number:{pattern:/(^|\s)(?:[1-9]\d*|0)(?:[.,]\d+)?\b/,lookbehind:!0}},a.inside=e.languages.bash;for(var s=["comment","function-name","for-or-select","assign-left","parameter","string","environment","function","keyword","builtin","boolean","file-descriptor","operator","punctuation","number"],o=n.variable[1].inside,i=0;i<s.length;i++)o[s[i]]=e.languages.bash[s[i]];e.languages.sh=e.languages.bash,e.languages.shell=e.languages.bash}(Prism);
|
|
12
|
+
!function(e){e.languages.diff={coord:[/^(?:\*{3}|-{3}|\+{3}).*$/m,/^@@.*@@$/m,/^\d.*$/m]};var n={"deleted-sign":"-","deleted-arrow":"<","inserted-sign":"+","inserted-arrow":">",unchanged:" ",diff:"!"};Object.keys(n).forEach((function(a){var i=n[a],r=[];/^\w+$/.test(a)||r.push(/\w+/.exec(a)[0]),"diff"===a&&r.push("bold"),e.languages.diff[a]={pattern:RegExp("^(?:["+i+"].*(?:\r\n?|\n|(?![\\s\\S])))+","m"),alias:r,inside:{line:{pattern:/(.)(?=[\s\S]).*(?:\r\n?|\n)?/,lookbehind:!0},prefix:{pattern:/[\s\S]/,alias:/\w+/.exec(a)[0]}}}})),Object.defineProperty(e.languages.diff,"PREFIXES",{value:n})}(Prism);
|
|
13
|
+
Prism.languages.python={comment:{pattern:/(^|[^\\])#.*/,lookbehind:!0,greedy:!0},"string-interpolation":{pattern:/(?:f|fr|rf)(?:("""|''')[\s\S]*?\1|("|')(?:\\.|(?!\2)[^\\\r\n])*\2)/i,greedy:!0,inside:{interpolation:{pattern:/((?:^|[^{])(?:\{\{)*)\{(?!\{)(?:[^{}]|\{(?!\{)(?:[^{}]|\{(?!\{)(?:[^{}])+\})+\})+\}/,lookbehind:!0,inside:{"format-spec":{pattern:/(:)[^:(){}]+(?=\}$)/,lookbehind:!0},"conversion-option":{pattern://,alias:"punctuation"},rest:null}},string:/[\s\S]+/}},"triple-quoted-string":{pattern:/(?:[rub]|br|rb)?("""|''')[\s\S]*?\1/i,greedy:!0,alias:"string"},string:{pattern:/(?:[rub]|br|rb)?("|')(?:\\.|(?!\1)[^\\\r\n])*\1/i,greedy:!0},function:{pattern:/((?:^|\s)def[ \t]+)[a-zA-Z_]\w*(?=\s*\()/g,lookbehind:!0},"class-name":{pattern:/(\bclass\s+)\w+/i,lookbehind:!0},decorator:{pattern:/(^[\t ]*)@\w+(?:\.\w+)*/m,lookbehind:!0,alias:["annotation","punctuation"],inside:{punctuation:/\./}},keyword:/\b(?:_(?=\s*:)|and|as|assert|async|await|break|case|class|continue|def|del|elif|else|except|exec|finally|for|from|global|if|import|in|is|lambda|match|nonlocal|not|or|pass|print|raise|return|try|while|with|yield)\b/,builtin:/\b(?:__import__|abs|all|any|apply|ascii|basestring|bin|bool|buffer|bytearray|bytes|callable|chr|classmethod|cmp|coerce|compile|complex|delattr|dict|dir|divmod|enumerate|eval|execfile|file|filter|float|format|frozenset|getattr|globals|hasattr|hash|help|hex|id|input|int|intern|isinstance|issubclass|iter|len|list|locals|long|map|max|memoryview|min|next|object|oct|open|ord|pow|property|range|raw_input|reduce|reload|repr|reversed|round|set|setattr|slice|sorted|staticmethod|str|sum|super|tuple|type|unichr|unicode|vars|xrange|zip)\b/,boolean:/\b(?:False|None|True)\b/,number:/\b0(?:b(?:_?[01])+|o(?:_?[0-7])+|x(?:_?[a-f0-9])+)\b|(?:\b\d+(?:_\d+)*(?:\.(?:\d+(?:_\d+)*)?)?|\B\.\d+(?:_\d+)*)(?:e[+-]?\d+(?:_\d+)*)?j?(?!\w)/i,operator:/[-+%=]=?|!=|:=|\*\*?=?|\/\/?=?|<[<=>]?|>[=>]?|[&|^~]/,punctuation:/[{}[\];(),.:]/},Prism.languages.python["string-interpolation"].inside.interpolation.inside.rest=Prism.languages.python,Prism.languages.py=Prism.languages.python;
|
package/trajectory/src/export.ts
CHANGED
|
@@ -27,15 +27,16 @@ export async function exportTrajectoryRunHtml(options: TrajectoryExportOptions):
|
|
|
27
27
|
const run = runs.find((candidate) => candidate.run.id === options.runId);
|
|
28
28
|
if (!run) throw new Error(`Workflow run ${options.runId} was not found for session ${options.sessionId}`);
|
|
29
29
|
const assets = new URL("./assets/", import.meta.url);
|
|
30
|
-
const [html, marked, morphdom, favicon] = await Promise.all([
|
|
30
|
+
const [html, marked, morphdom, prism, favicon] = await Promise.all([
|
|
31
31
|
readFile(new URL("index.html", assets), "utf8"),
|
|
32
32
|
readFile(new URL("marked.min.js", assets)),
|
|
33
33
|
readFile(new URL("morphdom.min.js", assets)),
|
|
34
|
+
readFile(new URL("prism.min.js", assets)),
|
|
34
35
|
readFile(new URL("favicon.png", assets)),
|
|
35
36
|
]);
|
|
36
37
|
const state = {
|
|
37
38
|
type: "state",
|
|
38
|
-
publishers: [{ id: `export-${options.runId}`, title: `run ${options.runId.slice(0, 8)}`, cwd: options.cwd, sessionId: options.sessionId,
|
|
39
|
+
publishers: [{ id: `export-${options.runId}`, title: `run ${options.runId.slice(0, 8)}`, cwd: options.cwd, sessionId: options.sessionId, connected: true, runs: [run], subagents: [] }],
|
|
39
40
|
updatedAt: Date.now(),
|
|
40
41
|
};
|
|
41
42
|
// Base64 data URLs avoid </script> escaping issues inside inlined sources; JSON gets the standard \u003c escape.
|
|
@@ -44,6 +45,7 @@ export async function exportTrajectoryRunHtml(options: TrajectoryExportOptions):
|
|
|
44
45
|
output = inlineAsset(output, '<link rel="icon" type="image/png" href="./favicon.png" />', `<link rel="icon" type="image/png" href="data:image/png;base64,${favicon.toString("base64")}" />`);
|
|
45
46
|
output = inlineAsset(output, '<script src="./marked.min.js"></script>', script(marked));
|
|
46
47
|
output = inlineAsset(output, '<script src="./morphdom.min.js"></script>', script(morphdom));
|
|
48
|
+
output = inlineAsset(output, '<script src="./prism.min.js"></script>', script(prism));
|
|
47
49
|
output = inlineAsset(output, '<body data-view="run">', `<body data-view="run">\n <script>window.__PIEWF_STATIC__ = ${JSON.stringify(state).replace(/</g, "\\u003c")};</script>`);
|
|
48
50
|
return output;
|
|
49
51
|
}
|
package/trajectory/src/index.ts
CHANGED
|
@@ -45,12 +45,18 @@ function publisherId(cwd: string, sessionId: string): string { return createHash
|
|
|
45
45
|
function trajectoryPort(value: unknown): number { return positiveInteger(value) && value <= 65535 ? value : DEFAULT_TRAJECTORY_PORT; }
|
|
46
46
|
function delay(ms: number): Promise<void> { return new Promise((resolve) => setTimeout(resolve, ms)); }
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
type ServerHealth = { pid?: number; fingerprint?: string; startedAt?: number };
|
|
49
|
+
// Servers older than the identity fields answer `{ ok: true }` alone, so every field is optional.
|
|
50
|
+
async function serverHealth(port: number): Promise<ServerHealth | undefined> {
|
|
49
51
|
try {
|
|
50
52
|
const response = await fetch(`http://127.0.0.1:${String(port)}/health`, { signal: AbortSignal.timeout(300) });
|
|
51
|
-
|
|
52
|
-
|
|
53
|
+
if (!response.ok) return undefined;
|
|
54
|
+
const body: unknown = await response.json().catch(() => undefined);
|
|
55
|
+
if (!object(body)) return {};
|
|
56
|
+
return { ...(positiveInteger(body.pid) ? { pid: body.pid } : {}), ...(typeof body.fingerprint === "string" ? { fingerprint: body.fingerprint } : {}), ...(positiveInteger(body.startedAt) ? { startedAt: body.startedAt } : {}) };
|
|
57
|
+
} catch { return undefined; }
|
|
53
58
|
}
|
|
59
|
+
async function serverHealthy(port: number): Promise<boolean> { return await serverHealth(port) !== undefined; }
|
|
54
60
|
|
|
55
61
|
function signalProcess(pid: number, signal: NodeJS.Signals): void {
|
|
56
62
|
try {
|
|
@@ -88,23 +94,28 @@ async function readLock(path: string): Promise<TrajectoryLock | undefined> {
|
|
|
88
94
|
}
|
|
89
95
|
}
|
|
90
96
|
|
|
91
|
-
|
|
97
|
+
// Only a server reporting this fingerprint counts: an older one still bound to the port would otherwise be adopted forever.
|
|
98
|
+
async function waitForServer(port: number, fingerprint: string): Promise<void> {
|
|
99
|
+
let foreign = false;
|
|
92
100
|
for (let attempt = 0; attempt < 60; attempt += 1) {
|
|
93
|
-
|
|
101
|
+
const health = await serverHealth(port);
|
|
102
|
+
if (health?.fingerprint === fingerprint) return;
|
|
103
|
+
foreign = health !== undefined;
|
|
94
104
|
await delay(50);
|
|
95
105
|
}
|
|
96
|
-
throw new Error(`Trajectory server did not start on port ${String(port)}`);
|
|
106
|
+
throw new Error(foreign ? `Trajectory port ${String(port)} is held by another Trajectory server; stop it and retry` : `Trajectory server did not start on port ${String(port)}`);
|
|
97
107
|
}
|
|
98
108
|
async function resolveExistingServer(lockPath: string, existing: TrajectoryLock, fingerprint: string): Promise<TrajectoryLock | undefined> {
|
|
99
|
-
|
|
100
|
-
|
|
109
|
+
const health = await serverHealth(existing.port);
|
|
110
|
+
if (health) {
|
|
111
|
+
if (existing.fingerprint === fingerprint && health.fingerprint === fingerprint) return existing;
|
|
101
112
|
await stopStaleServer(existing);
|
|
102
113
|
await rm(lockPath, { force: true });
|
|
103
114
|
return undefined;
|
|
104
115
|
}
|
|
105
116
|
if (await processAlive(existing.pid, existing.startedAt)) {
|
|
106
117
|
try {
|
|
107
|
-
await waitForServer(existing.port);
|
|
118
|
+
await waitForServer(existing.port, fingerprint);
|
|
108
119
|
return existing;
|
|
109
120
|
} catch {
|
|
110
121
|
// A live lock can still name the attaching process during startup; after the bounded wait, replace the unrecoverable startup owner and retry normally.
|
|
@@ -145,12 +156,15 @@ async function ensureTrajectoryServer(agentDir: string, configuredPort: number):
|
|
|
145
156
|
throw error;
|
|
146
157
|
}
|
|
147
158
|
} finally { await lockHandle?.close(); }
|
|
159
|
+
// A server can answer on the port without owning the lock, e.g. one whose lock was lost; it reports its pid, so replace it before spawning.
|
|
160
|
+
const occupant = await serverHealth(configuredPort);
|
|
161
|
+
if (occupant?.pid !== undefined && occupant.fingerprint !== fingerprint) await stopStaleServer({ pid: occupant.pid, port: configuredPort, ...(occupant.startedAt === undefined ? {} : { startedAt: occupant.startedAt }) });
|
|
148
162
|
try {
|
|
149
163
|
// NOTE: under a Bun-compiled pi binary process.execPath is the pi CLI, and Bun's node:http never writes the WebSocket 101 upgrade (oven-sh/bun#28157), so the server must run on a real node from PATH.
|
|
150
164
|
const child = spawn(process.versions.bun ? "node" : process.execPath, [serverPath, "--port", String(configuredPort), "--lock", lockPath, "--fingerprint", fingerprint], { detached: true, stdio: "ignore" });
|
|
151
165
|
const startupError = new Promise<never>((_resolve, reject) => { child.once("error", reject); });
|
|
152
166
|
child.unref();
|
|
153
|
-
await Promise.race([waitForServer(configuredPort), startupError]);
|
|
167
|
+
await Promise.race([waitForServer(configuredPort, fingerprint), startupError]);
|
|
154
168
|
return { port: configuredPort };
|
|
155
169
|
} catch (error) {
|
|
156
170
|
await rm(lockPath, { force: true });
|
|
@@ -180,6 +194,9 @@ const MAX_FRAME_BYTES = 32 * 1024 * 1024;
|
|
|
180
194
|
const MAX_LIVE_STATE_BYTES = MAX_FRAME_BYTES - 1024;
|
|
181
195
|
const MAX_TRANSCRIPT_REQUESTS = 64;
|
|
182
196
|
const TRANSCRIPT_REQUEST_TIMEOUT_MS = 10_000;
|
|
197
|
+
const ACTIVE_POLL_MS = 1_000;
|
|
198
|
+
// Nobody is watching, so the publisher stops reading run state every second.
|
|
199
|
+
const IDLE_POLL_MS = 10_000;
|
|
183
200
|
const RECONNECT_INITIAL_DELAY_MS = 100;
|
|
184
201
|
const RECONNECT_MAX_DELAY_MS = 5_000;
|
|
185
202
|
type LiveStateRecord = Record<string, unknown>;
|
|
@@ -197,40 +214,82 @@ const MAX_LIVE_OBJECT_KEYS = 64;
|
|
|
197
214
|
const LIVE_METADATA_ARRAY_KEYS = new Set(["agents", "runs", "subagents"]);
|
|
198
215
|
const LIVE_TRUNCATABLE_ARRAY_KEYS = new Set(["events", "phaseHistory"]);
|
|
199
216
|
const LIVE_METADATA_OBJECT_KEYS = new Set(["transcripts"]);
|
|
217
|
+
function liveValueWillBeBounded(value: unknown, key = "", depth = 0): boolean {
|
|
218
|
+
if (typeof value === "string") return Buffer.byteLength(value) > MAX_LIVE_STRING_BYTES;
|
|
219
|
+
if (typeof value !== "object" || value === null) return false;
|
|
220
|
+
if (depth >= 12) return true;
|
|
221
|
+
if (Array.isArray(value)) {
|
|
222
|
+
const maxEntries = LIVE_TRUNCATABLE_ARRAY_KEYS.has(key) ? MAX_LIVE_ARRAY_ENTRIES - 1 : MAX_LIVE_ARRAY_ENTRIES;
|
|
223
|
+
if (key === "attemptDetails" && value.length > 8 || !LIVE_METADATA_ARRAY_KEYS.has(key) && value.length > maxEntries) return true;
|
|
224
|
+
return value.some((entry) => liveValueWillBeBounded(entry, "", depth + 1));
|
|
225
|
+
}
|
|
226
|
+
if (!object(value)) return false;
|
|
227
|
+
const properties = LIVE_METADATA_OBJECT_KEYS.has(key) ? Object.keys(value) : Object.keys(value).sort().slice(0, MAX_LIVE_OBJECT_KEYS);
|
|
228
|
+
if (!LIVE_METADATA_OBJECT_KEYS.has(key) && Object.keys(value).length > MAX_LIVE_OBJECT_KEYS) return true;
|
|
229
|
+
return properties.some((property) => liveValueWillBeBounded(value[property], property, depth + 1));
|
|
230
|
+
}
|
|
231
|
+
const MAX_LIVE_TIMING_BYTES = 64 * 1024;
|
|
232
|
+
/** Keeps the newest timing entries: dropping the oldest leaves a live agent's gantt growing instead of frozen. */
|
|
200
233
|
function boundedTiming(value: unknown): unknown[] {
|
|
201
234
|
const entries = Array.isArray(value) ? value.filter(isTimingEntry) : [];
|
|
202
235
|
const retained: unknown[] = [];
|
|
203
236
|
let bytes = 2;
|
|
204
|
-
for (
|
|
237
|
+
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
205
238
|
let serialized: string;
|
|
206
|
-
try { serialized = JSON.stringify(
|
|
239
|
+
try { serialized = JSON.stringify(entries[index]); } catch { continue; }
|
|
207
240
|
const nextBytes = bytes + (retained.length ? 1 : 0) + Buffer.byteLength(serialized);
|
|
208
|
-
if (nextBytes >=
|
|
209
|
-
retained.push(
|
|
241
|
+
if (nextBytes >= MAX_LIVE_TIMING_BYTES) break;
|
|
242
|
+
retained.push(entries[index]);
|
|
210
243
|
bytes = nextBytes;
|
|
211
244
|
}
|
|
212
|
-
return retained;
|
|
245
|
+
return retained.reverse();
|
|
213
246
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
if (
|
|
218
|
-
|
|
219
|
-
if (
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
return
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
247
|
+
type LiveBounds = { argsTruncatedRuns: Set<number>; path: string[]; runIndex: number | undefined };
|
|
248
|
+
function boundedLiveValue(value: unknown, key = "", depth = 0, bounds?: LiveBounds): unknown {
|
|
249
|
+
const pathLength = bounds?.path.length ?? 0;
|
|
250
|
+
if (bounds && key) bounds.path.push(key);
|
|
251
|
+
const argsValue = bounds !== undefined && bounds.path.length >= 3 && bounds.path[0] === "runs" && bounds.path[1] === "snapshot" && bounds.path[2] === "args";
|
|
252
|
+
const markArgsTruncated = () => { if (argsValue && bounds.runIndex !== undefined) bounds.argsTruncatedRuns.add(bounds.runIndex); };
|
|
253
|
+
try {
|
|
254
|
+
if (typeof value === "string") {
|
|
255
|
+
if (argsValue && Buffer.byteLength(value) > MAX_LIVE_STRING_BYTES) markArgsTruncated();
|
|
256
|
+
return boundedString(value, MAX_LIVE_STRING_BYTES);
|
|
257
|
+
}
|
|
258
|
+
if (typeof value === "number" || typeof value === "boolean" || value === null) return value;
|
|
259
|
+
if (depth >= 12) { markArgsTruncated(); return undefined; }
|
|
260
|
+
if (key === "timing" && !argsValue) return boundedTiming(value);
|
|
261
|
+
if (key === "output" && object(value) && value.status === "available") {
|
|
262
|
+
const boundedOutput = boundedLiveValue(value.value, "value", depth + 1, bounds);
|
|
263
|
+
return liveValueWillBeBounded(value.value, "value", depth + 1) ? { status: "truncated", kind: "result", bytes: value.bytes } : { ...value, value: boundedOutput };
|
|
264
|
+
}
|
|
265
|
+
if (Array.isArray(value)) {
|
|
266
|
+
const array = value as readonly unknown[];
|
|
267
|
+
const maxEntries = !argsValue && LIVE_TRUNCATABLE_ARRAY_KEYS.has(key) ? MAX_LIVE_ARRAY_ENTRIES - 1 : MAX_LIVE_ARRAY_ENTRIES;
|
|
268
|
+
const entries = !argsValue && key === "attemptDetails" ? array.slice(-8) : !argsValue && LIVE_METADATA_ARRAY_KEYS.has(key) ? array : array.length <= maxEntries ? array : [...array.slice(0, 32), ...array.slice(-maxEntries + 32)];
|
|
269
|
+
if (entries.length !== array.length) markArgsTruncated();
|
|
270
|
+
const publisherRuns = bounds?.path.length === 1 && bounds.path[0] === "runs";
|
|
271
|
+
const bounded = entries.map((entry, index) => {
|
|
272
|
+
const previousRunIndex = bounds?.runIndex;
|
|
273
|
+
if (publisherRuns) bounds.runIndex = index;
|
|
274
|
+
const result = boundedLiveValue(entry, "", depth + 1, bounds);
|
|
275
|
+
if (bounds) bounds.runIndex = previousRunIndex;
|
|
276
|
+
return result;
|
|
277
|
+
});
|
|
278
|
+
if (entries.length !== array.length && LIVE_TRUNCATABLE_ARRAY_KEYS.has(key)) bounded.push({ type: "trajectory:truncated", field: key, omitted: array.length - entries.length });
|
|
279
|
+
return bounded;
|
|
280
|
+
}
|
|
281
|
+
if (object(value)) {
|
|
282
|
+
const result: LiveStateRecord = {};
|
|
283
|
+
const keys = Object.keys(value).sort();
|
|
284
|
+
const properties = !argsValue && LIVE_METADATA_OBJECT_KEYS.has(key) ? keys : keys.slice(0, MAX_LIVE_OBJECT_KEYS);
|
|
285
|
+
if (properties.length !== keys.length) markArgsTruncated();
|
|
286
|
+
for (const property of properties) result[property] = boundedLiveValue(value[property], property, depth + 1, bounds);
|
|
287
|
+
return result;
|
|
288
|
+
}
|
|
289
|
+
return undefined;
|
|
290
|
+
} finally {
|
|
291
|
+
if (bounds) bounds.path.length = pathLength;
|
|
232
292
|
}
|
|
233
|
-
return undefined;
|
|
234
293
|
}
|
|
235
294
|
function transcriptRevisionProjection(value: unknown, depth = 0): unknown {
|
|
236
295
|
if (depth >= 8) return typeof value === "string" ? boundedString(value, 1024) : typeof value === "number" || typeof value === "boolean" || value === null ? value : undefined;
|
|
@@ -276,8 +335,20 @@ function projectPublisher(metadata: TrajectoryPublisherMetadata, publisher: Live
|
|
|
276
335
|
const publisherId = typeof publisher.id === "string" ? publisher.id : "";
|
|
277
336
|
return { ...publisher, runs: metadata.runs.map((run) => projectRun(run as unknown as LiveStateRecord, `${publisherId}\t${run.run.id}`, revisions)), subagents: metadata.subagents.map((subagent) => projectSubagent(subagent as unknown as LiveStateRecord, `${publisherId}\tsubagent\t${subagent.id}`, revisions)) };
|
|
278
337
|
}
|
|
279
|
-
function minimalStatePublisher(publisher: LiveStateRecord): LiveStateRecord {
|
|
280
|
-
|
|
338
|
+
export function minimalStatePublisher(publisher: LiveStateRecord): LiveStateRecord {
|
|
339
|
+
const bounds: LiveBounds = { argsTruncatedRuns: new Set(), path: [], runIndex: undefined };
|
|
340
|
+
const bounded = (boundedLiveValue(publisher, "", 0, bounds) as LiveStateRecord | undefined) ?? {};
|
|
341
|
+
const sourceRuns: readonly unknown[] = Array.isArray(publisher.runs) ? publisher.runs : [];
|
|
342
|
+
const boundedRuns: readonly unknown[] = Array.isArray(bounded.runs) ? bounded.runs : [];
|
|
343
|
+
if (!sourceRuns.length || !boundedRuns.length) return bounded;
|
|
344
|
+
const runs = boundedRuns.map((run, index) => {
|
|
345
|
+
const sourceRun = sourceRuns[index];
|
|
346
|
+
const sourceSnapshot = object(sourceRun) && object(sourceRun.snapshot) ? sourceRun.snapshot : undefined;
|
|
347
|
+
const boundedSnapshot = object(run) && object(run.snapshot) ? run.snapshot : undefined;
|
|
348
|
+
const argsUnavailable = sourceSnapshot !== undefined && sourceSnapshot.args !== undefined && sourceSnapshot.args !== null && (boundedSnapshot === undefined || !Object.prototype.hasOwnProperty.call(boundedSnapshot, "args"));
|
|
349
|
+
return (bounds.argsTruncatedRuns.has(index) || argsUnavailable) && object(run) ? { ...run, snapshotArgsTruncated: true } : run;
|
|
350
|
+
});
|
|
351
|
+
return { ...bounded, runs };
|
|
281
352
|
}
|
|
282
353
|
function publisherStateFrame(publisher: LiveStateRecord, runs: readonly unknown[], subagents: readonly unknown[], truncated = false): LiveStateRecord {
|
|
283
354
|
const publisherSummary = { ...publisher };
|
|
@@ -331,10 +402,11 @@ export function createTrajectoryController(agentDir: string): TrajectoryControll
|
|
|
331
402
|
const pendingTranscripts = new Map<string, PendingTranscript>();
|
|
332
403
|
const stopPolling = () => { if (pollTimer !== undefined) { clearInterval(pollTimer); pollTimer = undefined; } };
|
|
333
404
|
const clearTranscriptRequests = () => { for (const request of pendingTranscripts.values()) clearTimeout(request.timer); pendingTranscripts.clear(); };
|
|
334
|
-
|
|
405
|
+
let viewers = 0;
|
|
406
|
+
const startPolling = () => { stopPolling(); pollTimer = setInterval(() => { void sendState().catch((error: unknown) => { if (!closing) console.error(`Trajectory state publish failed: ${errorText(error)}`); }); }, viewers > 0 ? ACTIVE_POLL_MS : IDLE_POLL_MS); pollTimer.unref(); };
|
|
335
407
|
let stateLoad: { socket: TrajectoryPublisherClient; task: Promise<void> } | undefined;
|
|
336
408
|
let lastState: string | undefined;
|
|
337
|
-
const publisherValue = (input: TrajectoryPublisherInput): LiveStateRecord => ({ id: publisherId(input.cwd, input.sessionId), title: `session ${input.sessionId.slice(0, 8)}`, cwd: input.cwd, sessionId: input.sessionId,
|
|
409
|
+
const publisherValue = (input: TrajectoryPublisherInput): LiveStateRecord => ({ id: publisherId(input.cwd, input.sessionId), title: `session ${input.sessionId.slice(0, 8)}`, cwd: input.cwd, sessionId: input.sessionId, connected: true });
|
|
338
410
|
const sendState = async (): Promise<void> => {
|
|
339
411
|
const activeSocket = socket;
|
|
340
412
|
if (activeSocket !== undefined && stateLoad?.socket === activeSocket) return stateLoad.task;
|
|
@@ -463,6 +535,14 @@ export function createTrajectoryController(agentDir: string): TrajectoryControll
|
|
|
463
535
|
const message: unknown = JSON.parse(typeof event === "object" && event !== null && "data" in event ? String(event.data) : "");
|
|
464
536
|
if (!object(message)) return;
|
|
465
537
|
if (message.type === "publisher:replaced") { established = false; return; }
|
|
538
|
+
if (message.type === "publisher:viewers" && typeof message.count === "number") {
|
|
539
|
+
const watched = viewers > 0;
|
|
540
|
+
viewers = message.count;
|
|
541
|
+
if (watched === viewers > 0) return;
|
|
542
|
+
if (socket === next && !closing) startPolling();
|
|
543
|
+
if (viewers > 0) void sendState().catch(() => undefined);
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
466
546
|
if (message.type === "publisher:transcript" && typeof message.requestId === "string" && typeof message.publisherId === "string" && (typeof message.runId === "string" && typeof message.agentId === "string" || typeof message.subagentId === "string")) { handleTranscriptRequest(next, generation, message, input); return; }
|
|
467
547
|
if (message.type !== "publisher:action" || typeof message.requestId !== "string") return;
|
|
468
548
|
const sendActionResponse = (response: LiveStateRecord): void => { if (socket !== next || closing || next.readyState !== 1) return; let serialized: string; try { serialized = JSON.stringify(response); } catch { serialized = JSON.stringify({ type: "publisher:action-result", requestId: response.requestId, publisherId: response.publisherId, ok: false, error: "Trajectory action result is invalid" }); } if (Buffer.byteLength(serialized) >= MAX_FRAME_BYTES) serialized = JSON.stringify({ type: "publisher:action-result", requestId: response.requestId, publisherId: response.publisherId, ok: false, error: "Trajectory action result is too large" }); next.send(serialized); };
|
package/trajectory/src/server.ts
CHANGED
|
@@ -9,7 +9,8 @@ const TRAJECTORY_IDLE_EXIT_MS = 5 * 60 * 1000;
|
|
|
9
9
|
|
|
10
10
|
type Socket = import("node:stream").Duplex;
|
|
11
11
|
type ClientKind = "publisher" | "browser";
|
|
12
|
-
type
|
|
12
|
+
type RunFocus = { publisherId: string; runId: string };
|
|
13
|
+
type Client = { socket: Socket; kind: ClientKind; publisherId?: string; buffer: Buffer; pendingState: Buffer | undefined; pendingDeliver: (() => void) | undefined; backpressured: boolean; superseded: boolean; focusAware?: boolean; focus?: RunFocus; sentTiming?: Map<string, number> };
|
|
13
14
|
type State = { type: "state"; publishers: readonly unknown[]; updatedAt: number; initial?: boolean; truncated?: boolean };
|
|
14
15
|
type PendingRequest = { requestId: string; kind: "transcript" | "action"; browser: Client; publisherId: string; publisher: Client; target: { runId?: string; agentId?: string; subagentId?: string; revision?: number }; timer: ReturnType<typeof setTimeout> };
|
|
15
16
|
const MAX_FRAME_BYTES = 32 * 1024 * 1024;
|
|
@@ -39,6 +40,55 @@ function compactSubagent(subagent: unknown): unknown {
|
|
|
39
40
|
const record = subagent as { transcript?: unknown };
|
|
40
41
|
return { ...record, transcript: compactTranscript(record.transcript) };
|
|
41
42
|
}
|
|
43
|
+
function withoutTiming(run: unknown): unknown {
|
|
44
|
+
if (!run || typeof run !== "object" || Array.isArray(run)) return run;
|
|
45
|
+
const record = run as { transcripts?: unknown };
|
|
46
|
+
if (!record.transcripts || typeof record.transcripts !== "object" || Array.isArray(record.transcripts)) return run;
|
|
47
|
+
const transcripts: Record<string, unknown> = {};
|
|
48
|
+
for (const [id, entry] of Object.entries(record.transcripts as Record<string, unknown>)) transcripts[id] = entry && typeof entry === "object" && !Array.isArray(entry) ? { ...entry, timing: [] } : entry;
|
|
49
|
+
return { ...record, transcripts };
|
|
50
|
+
}
|
|
51
|
+
/** Timing already delivered at this revision is omitted; the browser keeps the entries it cached. */
|
|
52
|
+
function knownTiming(run: unknown, prefix: string, sent: Map<string, number>, mark: (key: string, revision: number) => void): unknown {
|
|
53
|
+
if (!run || typeof run !== "object" || Array.isArray(run)) return run;
|
|
54
|
+
const record = run as { transcripts?: unknown };
|
|
55
|
+
if (!record.transcripts || typeof record.transcripts !== "object" || Array.isArray(record.transcripts)) return run;
|
|
56
|
+
const transcripts: Record<string, unknown> = {};
|
|
57
|
+
for (const [id, entry] of Object.entries(record.transcripts as Record<string, unknown>)) {
|
|
58
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) { transcripts[id] = entry; continue; }
|
|
59
|
+
const value = entry as { revision?: unknown; timing?: unknown };
|
|
60
|
+
if (typeof value.revision !== "number" || !Array.isArray(value.timing)) { transcripts[id] = entry; continue; }
|
|
61
|
+
if (sent.get(`${prefix}\t${id}`) === value.revision) { const rest = { ...value }; delete rest.timing; transcripts[id] = rest; continue; }
|
|
62
|
+
mark(`${prefix}\t${id}`, value.revision);
|
|
63
|
+
transcripts[id] = entry;
|
|
64
|
+
}
|
|
65
|
+
return { ...record, transcripts };
|
|
66
|
+
}
|
|
67
|
+
function forgetTiming(run: unknown, prefix: string, sent: Map<string, number>): void {
|
|
68
|
+
if (!run || typeof run !== "object" || Array.isArray(run)) return;
|
|
69
|
+
const record = run as { transcripts?: unknown };
|
|
70
|
+
if (!record.transcripts || typeof record.transcripts !== "object" || Array.isArray(record.transcripts)) return;
|
|
71
|
+
for (const id of Object.keys(record.transcripts)) sent.delete(`${prefix}\t${id}`);
|
|
72
|
+
}
|
|
73
|
+
/** Tool-timing only drives the focused run's gantt, so every other run ships without it. */
|
|
74
|
+
function focusedState(state: State, focus: RunFocus | undefined, sent?: Map<string, number>, mark: (key: string, revision: number) => void = () => {}): State {
|
|
75
|
+
return {
|
|
76
|
+
...state,
|
|
77
|
+
publishers: state.publishers.map((publisher) => {
|
|
78
|
+
if (!publisher || typeof publisher !== "object" || Array.isArray(publisher)) return publisher;
|
|
79
|
+
const value = publisher as { id?: unknown; runs?: unknown };
|
|
80
|
+
const runs: readonly unknown[] | undefined = Array.isArray(value.runs) ? value.runs as readonly unknown[] : undefined;
|
|
81
|
+
if (!runs) return publisher;
|
|
82
|
+
const focusedPublisher = focus !== undefined && value.id === focus.publisherId;
|
|
83
|
+
return { ...value, runs: runs.map((run) => {
|
|
84
|
+
const id = run && typeof run === "object" && !Array.isArray(run) ? (run as { run?: { id?: unknown } }).run?.id : undefined;
|
|
85
|
+
const prefix = `${String(value.id)}\t${String(id)}`;
|
|
86
|
+
if (!focusedPublisher || id !== focus.runId) { if (sent) forgetTiming(run, prefix, sent); return withoutTiming(run); }
|
|
87
|
+
return sent ? knownTiming(run, prefix, sent, mark) : run;
|
|
88
|
+
}) };
|
|
89
|
+
}),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
42
92
|
function compactPublishers(publishers: readonly unknown[]): unknown[] {
|
|
43
93
|
return publishers.map((publisher) => {
|
|
44
94
|
if (!publisher || typeof publisher !== "object" || Array.isArray(publisher)) return publisher;
|
|
@@ -77,7 +127,7 @@ function minimalPublisher(publisher: unknown): unknown {
|
|
|
77
127
|
function tinyPublisher(publisher: unknown): unknown {
|
|
78
128
|
if (!publisher || typeof publisher !== "object" || Array.isArray(publisher)) return { id: "publisher" };
|
|
79
129
|
const value = publisher as Record<string, unknown>;
|
|
80
|
-
return { id: typeof value.id === "string" ? value.id.slice(0, 200) : "publisher", title: typeof value.title === "string" ? value.title.slice(0, 200) : undefined, cwd: typeof value.cwd === "string" ? value.cwd.slice(0, 1024) : undefined, sessionId: typeof value.sessionId === "string" ? value.sessionId.slice(0, 200) : undefined,
|
|
130
|
+
return { id: typeof value.id === "string" ? value.id.slice(0, 200) : "publisher", title: typeof value.title === "string" ? value.title.slice(0, 200) : undefined, cwd: typeof value.cwd === "string" ? value.cwd.slice(0, 1024) : undefined, sessionId: typeof value.sessionId === "string" ? value.sessionId.slice(0, 200) : undefined, connected: value.connected === true };
|
|
81
131
|
}
|
|
82
132
|
function encodePublisherList(publishers: readonly unknown[], updatedAt: number, maxBytes: number, initial: boolean, truncated: boolean, projector: (publisher: unknown) => unknown, fallback?: (publisher: unknown) => unknown): { serialized: string; truncated: boolean } {
|
|
83
133
|
const prefix = `{"type":"state","publishers":[`;
|
|
@@ -133,11 +183,13 @@ function isState(value: unknown): value is State {
|
|
|
133
183
|
return Boolean(value && typeof value === "object" && !Array.isArray(value) && (value as { type?: unknown }).type === "state");
|
|
134
184
|
}
|
|
135
185
|
|
|
136
|
-
function writeFrame(client: Client, packet: Buffer, state: boolean): void {
|
|
137
|
-
|
|
186
|
+
function writeFrame(client: Client, packet: Buffer, state: boolean, onDelivered?: () => void): void {
|
|
187
|
+
// A superseded pending frame is never sent, so its payload must not count as delivered.
|
|
188
|
+
if (state && client.backpressured) { client.pendingState = packet; client.pendingDeliver = onDelivered; return; }
|
|
138
189
|
try {
|
|
139
|
-
if (state) client.pendingState = undefined;
|
|
190
|
+
if (state) { client.pendingState = undefined; client.pendingDeliver = undefined; }
|
|
140
191
|
if (!client.socket.write(packet)) client.backpressured = true;
|
|
192
|
+
onDelivered?.();
|
|
141
193
|
} catch { client.socket.destroy(); }
|
|
142
194
|
}
|
|
143
195
|
|
|
@@ -194,6 +246,7 @@ type TrajectoryServerOptions = { maxFrameBytes?: number; fingerprint?: string };
|
|
|
194
246
|
export function createTrajectoryServer(port: number, lockPath: string, options: TrajectoryServerOptions = {}): Server {
|
|
195
247
|
const maxFrameBytes = options.maxFrameBytes ?? MAX_FRAME_BYTES;
|
|
196
248
|
const serverFingerprint = options.fingerprint ?? "";
|
|
249
|
+
let startedAt = 0;
|
|
197
250
|
const clients = new Set<Client>();
|
|
198
251
|
const publishers = new Map<string, { client: Client; value: Record<string, unknown>; generation: number }>();
|
|
199
252
|
let latest: State = { type: "state", publishers: [], updatedAt: Date.now(), initial: true };
|
|
@@ -234,8 +287,19 @@ export function createTrajectoryServer(port: number, lockPath: string, options:
|
|
|
234
287
|
const broadcast = (value: unknown) => {
|
|
235
288
|
if (isState(value)) {
|
|
236
289
|
try {
|
|
237
|
-
|
|
238
|
-
for (const client of clients)
|
|
290
|
+
let shared: Buffer | undefined;
|
|
291
|
+
for (const client of clients) {
|
|
292
|
+
if (client.kind !== "browser") continue;
|
|
293
|
+
if (client.focusAware !== true) {
|
|
294
|
+
shared ??= frame(encodeState(value, maxFrameBytes), maxFrameBytes);
|
|
295
|
+
writeFrame(client, shared, true);
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
const sent = (client.sentTiming ??= new Map<string, number>());
|
|
299
|
+
const marks: [string, number][] = [];
|
|
300
|
+
const stateFrame = frame(encodeState(focusedState(value, client.focus, sent, (key, revision) => marks.push([key, revision])), maxFrameBytes), maxFrameBytes);
|
|
301
|
+
writeFrame(client, stateFrame, true, () => { for (const [key, revision] of marks) sent.set(key, revision); });
|
|
302
|
+
}
|
|
239
303
|
} catch {
|
|
240
304
|
for (const client of clients) if (client.kind === "browser") client.socket.destroy();
|
|
241
305
|
}
|
|
@@ -243,6 +307,14 @@ export function createTrajectoryServer(port: number, lockPath: string, options:
|
|
|
243
307
|
}
|
|
244
308
|
for (const client of clients) if (client.kind === "browser") emit(client, value);
|
|
245
309
|
};
|
|
310
|
+
let viewers = 0;
|
|
311
|
+
// Publishers poll their run directory, so they only need a fast cadence while a browser is watching.
|
|
312
|
+
const notifyViewers = () => {
|
|
313
|
+
const count = [...clients].filter((candidate) => candidate.kind === "browser").length;
|
|
314
|
+
if (count === viewers) return;
|
|
315
|
+
viewers = count;
|
|
316
|
+
for (const candidate of clients) if (candidate.kind === "publisher") emit(candidate, { type: "publisher:viewers", count });
|
|
317
|
+
};
|
|
246
318
|
const publishState = () => {
|
|
247
319
|
const values = [...publishers.values()].map(({ value, generation }) => ({ ...value, generation }));
|
|
248
320
|
latest = { type: "state", publishers: values, updatedAt: Date.now() };
|
|
@@ -251,6 +323,7 @@ export function createTrajectoryServer(port: number, lockPath: string, options:
|
|
|
251
323
|
const disconnect = (client: Client) => {
|
|
252
324
|
clearPending(client, client.kind === "publisher" ? "Publisher is disconnected" : "Trajectory browser disconnected");
|
|
253
325
|
clients.delete(client);
|
|
326
|
+
if (client.kind === "browser") notifyViewers();
|
|
254
327
|
if (client.kind === "publisher" && client.publisherId && publishers.get(client.publisherId)?.client === client) {
|
|
255
328
|
publishers.delete(client.publisherId);
|
|
256
329
|
publishState();
|
|
@@ -264,7 +337,21 @@ export function createTrajectoryServer(port: number, lockPath: string, options:
|
|
|
264
337
|
const message = value as Record<string, unknown>;
|
|
265
338
|
if (client.superseded) return;
|
|
266
339
|
if (message.type === "publisher:detach" && client.kind === "publisher" && client.publisherId && publishers.get(client.publisherId)?.client === client) { clearPending(client, "Publisher is disconnected"); publishers.delete(client.publisherId); publishState(); scheduleIdleExit(); return; }
|
|
267
|
-
if (message.type === "ui:attach") { client.kind = "browser"; emit(client, latest); return; }
|
|
340
|
+
if (message.type === "ui:attach") { client.kind = "browser"; emit(client, latest); notifyViewers(); return; }
|
|
341
|
+
if (message.type === "ui:focus" && client.kind === "browser") {
|
|
342
|
+
const publisherId = typeof message.publisherId === "string" ? message.publisherId : undefined;
|
|
343
|
+
const runId = typeof message.runId === "string" ? message.runId : undefined;
|
|
344
|
+
client.focusAware = true;
|
|
345
|
+
client.sentTiming = new Map<string, number>();
|
|
346
|
+
if (publisherId !== undefined && runId !== undefined) client.focus = { publisherId, runId };
|
|
347
|
+
else delete client.focus;
|
|
348
|
+
const sent = client.sentTiming;
|
|
349
|
+
const marks: [string, number][] = [];
|
|
350
|
+
const focused = focusedState(latest, client.focus, sent, (key, revision) => marks.push([key, revision]));
|
|
351
|
+
try { writeFrame(client, frame(encodeState(focused, maxFrameBytes), maxFrameBytes), true, () => { for (const [key, revision] of marks) sent.set(key, revision); }); }
|
|
352
|
+
catch { client.socket.destroy(); }
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
268
355
|
if (client.kind === "publisher" && message.type === "publisher:attach" && typeof message.publisherId === "string") {
|
|
269
356
|
cancelIdleExit();
|
|
270
357
|
client.publisherId = message.publisherId;
|
|
@@ -277,6 +364,7 @@ export function createTrajectoryServer(port: number, lockPath: string, options:
|
|
|
277
364
|
}
|
|
278
365
|
const generation = (previous?.generation ?? 0) + 1;
|
|
279
366
|
publishers.set(message.publisherId, { client, value: { ...(previous?.value ?? { id: message.publisherId }), id: message.publisherId, connected: true }, generation });
|
|
367
|
+
emit(client, { type: "publisher:viewers", count: viewers });
|
|
280
368
|
publishState();
|
|
281
369
|
return;
|
|
282
370
|
}
|
|
@@ -374,7 +462,8 @@ export function createTrajectoryServer(port: number, lockPath: string, options:
|
|
|
374
462
|
catch { writeJson(response, 400, { error: "Invalid request" }); return; }
|
|
375
463
|
if (!authorized(request, port)) { writeJson(response, 403, { error: "Forbidden" }); return; }
|
|
376
464
|
const path = url.pathname;
|
|
377
|
-
|
|
465
|
+
// The identity lets an attaching Pi tell its own server from any other one answering on the port.
|
|
466
|
+
if (request.method === "GET" && path === "/health") { writeJson(response, 200, { ok: true, pid: process.pid, fingerprint: serverFingerprint, startedAt }); return; }
|
|
378
467
|
if (request.method === "GET" && (path === "/" || path === "/index.html")) {
|
|
379
468
|
void readFile(new URL("./assets/index.html", import.meta.url)).then((html) => {
|
|
380
469
|
response.writeHead(200, { "content-type": "text/html; charset=utf-8", "content-length": html.byteLength, "cache-control": "no-store" });
|
|
@@ -396,6 +485,13 @@ export function createTrajectoryServer(port: number, lockPath: string, options:
|
|
|
396
485
|
}).catch(() => { writeJson(response, 500, { error: "Trajectory DOM diffing library is unavailable" }); });
|
|
397
486
|
return;
|
|
398
487
|
}
|
|
488
|
+
if (request.method === "GET" && path === "/prism.min.js") {
|
|
489
|
+
void readFile(new URL("./assets/prism.min.js", import.meta.url)).then((script) => {
|
|
490
|
+
response.writeHead(200, { "content-type": "application/javascript; charset=utf-8", "content-length": script.byteLength, "cache-control": "no-store" });
|
|
491
|
+
response.end(script);
|
|
492
|
+
}).catch(() => { writeJson(response, 500, { error: "Trajectory syntax highlighting library is unavailable" }); });
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
399
495
|
if (request.method === "GET" && (path === "/favicon.png" || path === "/favicon.ico")) {
|
|
400
496
|
void readFile(new URL("./assets/favicon.png", import.meta.url)).then((icon) => {
|
|
401
497
|
response.writeHead(200, { "content-type": "image/png", "content-length": icon.byteLength, "cache-control": "no-store" });
|
|
@@ -413,12 +509,14 @@ export function createTrajectoryServer(port: number, lockPath: string, options:
|
|
|
413
509
|
if (!authorized(request, port) || url.pathname !== "/ws" || typeof key !== "string") { socket.destroy(); return; }
|
|
414
510
|
const accept = createHash("sha1").update(`${key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11`).digest("base64");
|
|
415
511
|
socket.write(`HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: ${accept}\r\n\r\n`);
|
|
416
|
-
const client: Client = { socket, kind: "publisher", buffer: Buffer.alloc(0), pendingState: undefined, backpressured: false, superseded: false };
|
|
512
|
+
const client: Client = { socket, kind: "publisher", buffer: Buffer.alloc(0), pendingState: undefined, pendingDeliver: undefined, backpressured: false, superseded: false };
|
|
417
513
|
socket.on("drain", () => {
|
|
418
514
|
const pendingState = client.pendingState;
|
|
515
|
+
const pendingDeliver = client.pendingDeliver;
|
|
419
516
|
client.pendingState = undefined;
|
|
517
|
+
client.pendingDeliver = undefined;
|
|
420
518
|
client.backpressured = false;
|
|
421
|
-
if (pendingState !== undefined) writeFrame(client, pendingState, true);
|
|
519
|
+
if (pendingState !== undefined) writeFrame(client, pendingState, true, pendingDeliver);
|
|
422
520
|
});
|
|
423
521
|
clients.add(client);
|
|
424
522
|
socket.on("data", (chunk: unknown) => {
|
|
@@ -427,8 +525,10 @@ export function createTrajectoryServer(port: number, lockPath: string, options:
|
|
|
427
525
|
});
|
|
428
526
|
socket.on("close", () => { disconnect(client); });
|
|
429
527
|
socket.on("error", () => { disconnect(client); });
|
|
528
|
+
// A dropped tab only sends FIN, and an upgraded socket stays half-open until this side closes it.
|
|
529
|
+
socket.on("end", () => { socket.destroy(); });
|
|
430
530
|
});
|
|
431
|
-
server.once("listening", () => { void writeFile(lockPath, `${JSON.stringify({ pid: process.pid, port, fingerprint: serverFingerprint, startedAt
|
|
531
|
+
server.once("listening", () => { startedAt = Date.now(); void writeFile(lockPath, `${JSON.stringify({ pid: process.pid, port, fingerprint: serverFingerprint, startedAt })}\n`, { mode: 0o600 }).catch(() => { process.exitCode = 1; }); scheduleIdleExit(); });
|
|
432
532
|
server.on("close", () => { closed = true; if (idleTimer !== undefined) { clearTimeout(idleTimer); idleTimer = undefined; } });
|
|
433
533
|
return server;
|
|
434
534
|
}
|