@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.
Files changed (71) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/dist/src/agent-execution.js +36 -51
  3. package/dist/src/background-widget.d.ts +56 -0
  4. package/dist/src/background-widget.js +22 -15
  5. package/dist/src/decoders.js +2 -1
  6. package/dist/src/host-navigator.d.ts +18 -0
  7. package/dist/src/host-navigator.js +71 -13
  8. package/dist/src/host-view.d.ts +16 -1
  9. package/dist/src/host-view.js +109 -42
  10. package/dist/src/host.js +57 -24
  11. package/dist/src/index.d.ts +3 -1
  12. package/dist/src/index.js +867 -545
  13. package/dist/src/index.js.map +3 -3
  14. package/dist/src/registry.js +2 -1
  15. package/dist/src/roles.d.ts +70 -0
  16. package/dist/src/roles.js +325 -0
  17. package/dist/src/settings.d.ts +13 -0
  18. package/dist/src/settings.js +253 -0
  19. package/dist/src/trajectory.d.ts +29 -4
  20. package/dist/src/trajectory.js +58 -3
  21. package/dist/src/types.d.ts +0 -1
  22. package/dist/src/utils.d.ts +2 -1
  23. package/dist/src/utils.js +10 -0
  24. package/dist/src/validation.d.ts +5 -17
  25. package/dist/src/validation.js +9 -458
  26. package/dist/subagents/index.js +1132 -694
  27. package/dist/subagents/index.js.map +3 -3
  28. package/dist/subagents/src/contracts.d.ts +4 -0
  29. package/dist/subagents/src/index.d.ts +1 -1
  30. package/dist/subagents/src/index.js +2 -1
  31. package/dist/subagents/src/manager.js +29 -10
  32. package/dist/subagents/src/navigator.js +396 -247
  33. package/dist/subagents/src/view.d.ts +23 -2
  34. package/dist/subagents/src/view.js +96 -30
  35. package/dist/trajectory/assets/index.html +347 -231
  36. package/dist/trajectory/assets/prism.min.js +13 -0
  37. package/dist/trajectory/index.js +377 -257
  38. package/dist/trajectory/index.js.map +3 -3
  39. package/dist/trajectory/src/assets/index.html +347 -231
  40. package/dist/trajectory/src/assets/prism.min.js +13 -0
  41. package/dist/trajectory/src/export.js +4 -2
  42. package/dist/trajectory/src/index.d.ts +2 -0
  43. package/dist/trajectory/src/index.js +143 -42
  44. package/dist/trajectory/src/server.js +134 -9
  45. package/dist/trajectory/src/server.js.map +1 -1
  46. package/package.json +2 -1
  47. package/src/agent-execution.ts +36 -42
  48. package/src/background-widget.ts +25 -18
  49. package/src/decoders.ts +2 -1
  50. package/src/host-navigator.ts +58 -14
  51. package/src/host-view.ts +93 -37
  52. package/src/host.ts +51 -21
  53. package/src/index.ts +3 -1
  54. package/src/registry.ts +2 -1
  55. package/src/roles.ts +326 -0
  56. package/src/settings.ts +206 -0
  57. package/src/trajectory.ts +76 -7
  58. package/src/types.ts +1 -1
  59. package/src/utils.ts +8 -1
  60. package/src/validation.ts +12 -356
  61. package/subagents/README.md +1 -1
  62. package/subagents/src/contracts.ts +4 -0
  63. package/subagents/src/index.ts +3 -2
  64. package/subagents/src/manager.ts +28 -10
  65. package/subagents/src/navigator.ts +336 -180
  66. package/subagents/src/view.ts +97 -28
  67. package/trajectory/src/assets/index.html +347 -231
  68. package/trajectory/src/assets/prism.min.js +13 -0
  69. package/trajectory/src/export.ts +4 -2
  70. package/trajectory/src/index.ts +118 -38
  71. 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,"&amp;").replace(/</g,"&lt;").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,"&quot;")+'"';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:/![sra](?=[:}]$)/,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;
@@ -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, themes: true, connected: true, runs: [run], subagents: [] }],
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
  }
@@ -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
- async function serverHealthy(port: number): Promise<boolean> {
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
- return response.ok;
52
- } catch { return false; }
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
- async function waitForServer(port: number): Promise<void> {
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
- if (await serverHealthy(port)) return;
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
- if (await serverHealthy(existing.port)) {
100
- if (existing.fingerprint === fingerprint) return existing;
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 (const entry of entries) {
237
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
205
238
  let serialized: string;
206
- try { serialized = JSON.stringify(entry); } catch { continue; }
239
+ try { serialized = JSON.stringify(entries[index]); } catch { continue; }
207
240
  const nextBytes = bytes + (retained.length ? 1 : 0) + Buffer.byteLength(serialized);
208
- if (nextBytes >= 64 * 1024) break;
209
- retained.push(entry);
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
- function boundedLiveValue(value: unknown, key = "", depth = 0): unknown {
215
- if (typeof value === "string") return boundedString(value, MAX_LIVE_STRING_BYTES);
216
- if (typeof value === "number" || typeof value === "boolean" || value === null) return value;
217
- if (depth >= 12) return undefined;
218
- if (key === "timing") return boundedTiming(value);
219
- if (Array.isArray(value)) {
220
- const array = value as readonly unknown[];
221
- const maxEntries = LIVE_TRUNCATABLE_ARRAY_KEYS.has(key) ? MAX_LIVE_ARRAY_ENTRIES - 1 : MAX_LIVE_ARRAY_ENTRIES;
222
- const entries = key === "attemptDetails" ? array.slice(-8) : LIVE_METADATA_ARRAY_KEYS.has(key) ? array : array.length <= maxEntries ? array : [...array.slice(0, 32), ...array.slice(-maxEntries + 32)];
223
- const bounded = entries.map((entry) => boundedLiveValue(entry, "", depth + 1));
224
- if (entries.length !== array.length && LIVE_TRUNCATABLE_ARRAY_KEYS.has(key)) bounded.push({ type: "trajectory:truncated", field: key, omitted: array.length - entries.length });
225
- return bounded;
226
- }
227
- if (object(value)) {
228
- const result: LiveStateRecord = {};
229
- const properties = LIVE_METADATA_OBJECT_KEYS.has(key) ? Object.keys(value).sort() : Object.keys(value).sort().slice(0, MAX_LIVE_OBJECT_KEYS);
230
- for (const property of properties) result[property] = boundedLiveValue(value[property], property, depth + 1);
231
- return result;
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
- return (boundedLiveValue(publisher) as LiveStateRecord | undefined) ?? {};
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
- const startPolling = () => { stopPolling(); pollTimer = setInterval(() => { void sendState().catch((error: unknown) => { if (!closing) console.error(`Trajectory state publish failed: ${errorText(error)}`); }); }, 1000); pollTimer.unref(); };
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, themes: input.themes, connected: true });
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); };
@@ -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 Client = { socket: Socket; kind: ClientKind; publisherId?: string; buffer: Buffer; pendingState: Buffer | undefined; backpressured: boolean; superseded: boolean };
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, themes: value.themes, connected: value.connected === true };
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
- if (state && client.backpressured) { client.pendingState = packet; return; }
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
- const stateFrame = frame(encodeState(value, maxFrameBytes), maxFrameBytes);
238
- for (const client of clients) if (client.kind === "browser") writeFrame(client, stateFrame, true);
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
- if (request.method === "GET" && path === "/health") { writeJson(response, 200, { ok: true }); return; }
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: Date.now() })}\n`, { mode: 0o600 }).catch(() => { process.exitCode = 1; }); scheduleIdleExit(); });
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
  }