peon-mem 1.0.7 → 1.0.8

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/dist/monitor.js CHANGED
@@ -23,12 +23,10 @@ const CLIENT_SCRIPT = String.raw `
23
23
  stale: "aged out — not recalled by default",
24
24
  conflicted: "contradicts another belief"
25
25
  };
26
- var ROUTES = ["brain","projects","overview","memory","roundtable","ops"];
26
+ var ROUTES = ["brain","projects","overview","memory","ops"];
27
27
  var ui = { project:"", filter:"all", search:null, focus:null, editing:null };
28
28
  var latest = null, overview = null, network = null, dash = null, busy = false;
29
29
  var ovLoadedAt = 0, netLoadedAt = 0, dashLoadedAt = 0, HEAVY_TTL = 12000;
30
- var rtRuns = [], rtAgents = null, rtLoadedAt = 0, rtSelected = null, rtBusy = false, rtDrafts = {}, rtWorkTabs = {};
31
- var rtStream = null, rtStreamProject = "";
32
30
  // Load state for the heavy endpoints. Without this, a page renders its EMPTY state while data is
33
31
  // still in flight — /network takes seconds on a large brain, so "No projects yet" was shown as
34
32
  // fact for the whole wait, and stayed forever if the request failed (the catch was silent).
@@ -99,19 +97,17 @@ const CLIENT_SCRIPT = String.raw `
99
97
 
100
98
  function renderRoute(force){
101
99
  var r=currentRoute();
102
- document.body.classList.toggle("room-theme",r==="roundtable");
103
100
  ROUTES.forEach(function(x){ var p=EL("page-"+x); if(p) p.hidden=(x!==r); });
104
101
  // The project dropdown is CONTEXT, not chrome: it only applies inside a project's
105
102
  // Insights/Memory pages. Global pages (Neural Core / Sectors / Systems) hide it —
106
103
  // switching projects there happens by picking a galaxy or a sector card.
107
- var sw=EL("switcher"); if(sw) sw.hidden=!(r==="overview"||r==="memory"||r==="roundtable");
104
+ var sw=EL("switcher"); if(sw) sw.hidden=!(r==="overview"||r==="memory");
108
105
  // Nav highlight: overview/memory belong under "projects".
109
- ["brain","projects","roundtable","ops"].forEach(function(x){ var t=EL("nav-"+x); if(t) t.classList.toggle("on", x===r || ((r==="overview"||r==="memory")&&x==="projects")); });
106
+ ["brain","projects","ops"].forEach(function(x){ var t=EL("nav-"+x); if(t) t.classList.toggle("on", x===r || ((r==="overview"||r==="memory")&&x==="projects")); });
110
107
  if(r==="brain"){ loadDashboard(force); renderBrainHome(); }
111
108
  else if(r==="projects"){ loadNetwork(force); renderProjectsGrid(); }
112
109
  else if(r==="overview"){ loadOverview(force); renderOverviewPage(); }
113
110
  else if(r==="memory") renderMemoryPage();
114
- else if(r==="roundtable") loadRoundtable(force);
115
111
  else if(r==="ops") renderOpsPage();
116
112
  }
117
113
 
@@ -678,149 +674,6 @@ const CLIENT_SCRIPT = String.raw `
678
674
  renderProjectsGrid();
679
675
  }
680
676
 
681
- // ===== ROUNDTABLE (Codex + Claude in a shared, alternating conversation) =====
682
- async function loadRoundtable(force){
683
- if(!ui.project){ renderRoundtablePage(); return; }
684
- if(!force&&rtStream&&rtStreamProject===ui.project)return;
685
- var active=rtRuns.some(function(r){return r.status==="queued"||r.status==="running"||r.status==="executing";});
686
- var ttl=active?1800:6000;
687
- if(!force && (freshNow()-rtLoadedAt)<ttl){ if(!roomInputFocused())renderRoundtablePage(); return; }
688
- rtLoadedAt=freshNow();
689
- try{
690
- var data=await Promise.all([
691
- fetch("/roundtable/status",{cache:"no-store"}).then(function(r){if(!r.ok)throw new Error("HTTP "+r.status);return r.json();}),
692
- fetch("/roundtable/runs?projectPath="+encodeURIComponent(ui.project),{cache:"no-store"}).then(function(r){if(!r.ok)throw new Error("HTTP "+r.status);return r.json();})
693
- ]);
694
- rtAgents=data[0].agents||null; rtRuns=data[1].runs||[];
695
- if(!rtSelected&&rtRuns.length) rtSelected=rtRuns[0].id;
696
- connectRoundtableStream();
697
- }catch(e){ rtAgents=null; }
698
- if(!roomInputFocused())renderRoundtablePage();
699
- }
700
- function roomInputFocused(){var a=document.activeElement;return !!(a&&a.hasAttribute&&a.hasAttribute("data-rt-message-input"));}
701
- function connectRoundtableStream(){
702
- if(!ui.project||!window.EventSource)return;
703
- if(rtStream&&rtStreamProject===ui.project)return;
704
- if(rtStream)rtStream.close();
705
- rtStreamProject=ui.project;rtStream=new EventSource("/roundtable/events?projectPath="+encodeURIComponent(ui.project));
706
- rtStream.onmessage=function(event){
707
- try{var run=JSON.parse(event.data),found=false;rtRuns=rtRuns.map(function(current){if(current.id===run.id){found=true;return run;}return current;});if(!found)rtRuns.unshift(run);rtRuns.sort(function(a,b){return b.createdAt.localeCompare(a.createdAt);});if(!rtSelected)rtSelected=run.id;if(!roomInputFocused())renderRoundtablePage();}catch(e){}
708
- };
709
- }
710
- function renderRoundtablePage(){
711
- var body=EL("rt-body"); if(!body) return;
712
- if(!ui.project){ body.innerHTML='<div class="empty">Select a project first.</div>'; return; }
713
- var ca=rtAgents&&rtAgents.codex, cl=rtAgents&&rtAgents.claude;
714
- var team='<section class="rtstudio-card rtteam"><div class="rtstudio-label">Research team</div><div class="rtteammate codex"><div class="rtminiavatar">C</div><div>'+agentBadge("Codex",ca)+'<p>Builds, tests, and challenges Claude.</p></div></div><div class="rtteammate claude"><div class="rtminiavatar">C</div><div>'+agentBadge("Claude",cl)+'<p>Investigates independently, challenges Codex, and audits the evidence.</p></div></div><small>Both agents receive the same room transcript and thesis context.</small></section>';
715
- var form='<details class="rtstudio-card rtstudio-new"><summary>Start a new conversation</summary><div class="rtnewbody"><div class="rtautodesc">Greet them, ask questions, explore ideas, or disagree. A conversation never approves or runs work.</div>'+
716
- '<textarea id="rt-question" maxlength="4000" placeholder="Optional topic — leave blank for an open thesis discussion."></textarea>'+
717
- '<div class="rtactions"><span class="mono" id="rt-msg">No implementation without approval.</span><button class="btn" id="rt-start"'+((!ca||!ca.available||!cl||!cl.available||rtBusy)?" disabled":"")+'>Start conversation</button></div></div></details>';
718
- var chosen=rtRuns.find(function(r){return r.id===rtSelected;})||rtRuns[0];
719
- var detail=chosen?roundtableRun(chosen):'<section class="panel"><div class="empty">No discussions yet. Start an autonomous roundtable when you are ready.</div></section>';
720
- var history='<section class="rtstudio-card rtstudio-history"><div class="rtstudio-sectionhead"><span>Conversations</span><small>'+rtRuns.length+' saved</small></div><div class="rthistory">'+(rtRuns.length?rtRuns.slice(0,12).map(function(r){var label=r.mode==="autonomous"?"Autonomous agenda from Peon memory":r.question;return '<button class="rthist'+(chosen&&chosen.id===r.id?' on':'')+'" data-rt-run="'+esc(r.id)+'"><span>'+esc(clip(label,58))+'</span><small class="rtstate '+esc(r.status)+'">'+esc(r.status)+'</small></button>';}).join(""):'<div class="rtworkempty">No saved conversations yet.</div>')+'</div></section>';
721
- var goal='<section class="rtstudio-card rtgoal"><span>Thesis objective</span><b>70% on Mini-Dev</b><strong>GPT-OSS-120B</strong><small>Produce at least one measurable, publishable contribution. Every implementation remains approval-gated.</small></section>';
722
- var checkpoint=chosen?'<section class="rtstudio-card rtcheckpoint"><span>Current checkpoint</span><b>'+esc(chosen.liveSummary||"The room is ready for the next evidence-backed step.")+'</b><small>'+esc(chosen.status==="open"?"Discussion is open":stageLabel(chosen.stage||chosen.status))+'</small></section>':'';
723
- var work=chosen?roundtableWorkbench(chosen):'<section class="rtworkbench"><div class="rtworkempty">Work activity will appear here once a conversation starts.</div></section>';
724
- body.innerHTML='<div class="rtstudio"><aside class="rtstudio-nav">'+goal+team+checkpoint+form+history+'</aside><main class="rtstudio-main">'+detail+'</main><aside class="rtstudio-work">'+work+'</aside></div>';
725
- var start=EL("rt-start"); if(start) start.addEventListener("click",startRoundtable);
726
- var q=EL("rt-question"); if(q) q.addEventListener("keydown",function(e){if((e.metaKey||e.ctrlKey)&&e.key==="Enter") startRoundtable();});
727
- Array.prototype.forEach.call(document.querySelectorAll("[data-rt-run]"),function(b){b.addEventListener("click",function(){rtSelected=b.getAttribute("data-rt-run");renderRoundtablePage();});});
728
- wireRoundtableActions();
729
- var transcript=document.querySelector(".rtturns");if(transcript)transcript.scrollTop=transcript.scrollHeight;
730
- }
731
- function agentBadge(label,a){ var ok=a&&a.available; return '<span class="rtagent '+(ok?'ok':'bad')+'"><i></i><b>'+esc(label)+'</b><small>'+esc(a?a.label:"checking client")+'</small></span>'; }
732
- function roundtableWorkbench(r){
733
- var turns=r.turns||[], activities=(r.activities||[]).slice();
734
- if(!activities.length){
735
- turns.forEach(function(t){
736
- if(t.agent==="user"||t.agent==="joint")return;
737
- var content=String(t.content||""), type="", title="";
738
- if(t.kind==="implementation"){type="code";title="Implementation report";}
739
- else if(t.kind==="review"){type="review";title="Independent review reported";}
740
- else if(t.kind==="verification"){type="test";title="Test or verification result";}
741
- if(type)activities.push({id:t.id,agent:t.agent,type:type,title:title,detail:content,state:/\b(fail|blocking problem|error)\b/i.test(content)?"failed":"info",createdAt:t.createdAt,updatedAt:t.createdAt});
742
- });
743
- }
744
- var tab=rtWorkTabs[r.id]||"all";
745
- var filtered=activities.filter(function(a){if(tab==="all")return true;if(tab==="code")return a.type==="code"||a.type==="artifact";if(tab==="tests")return a.type==="test"||a.type==="review";return a.type==="result";}).slice(-10).reverse();
746
- var hasImplementation=turns.some(function(t){return t.kind==="implementation";});
747
- var hasReview=turns.some(function(t){return t.kind==="review";});
748
- var hasVerification=turns.some(function(t){return t.kind==="verification";});
749
- function phase(label,state){return '<span class="rtworkphase '+state+'"><i></i>'+label+'</span>';}
750
- var phases=phase("Discuss",turns.length?"done":"waiting")+
751
- phase("Build",r.stage==="implementation"?"active":hasImplementation?"done":"waiting")+
752
- phase("Review",r.stage==="review"?"active":hasReview?"done":"waiting")+
753
- phase("Verify",r.stage==="verification"?"active":hasVerification?"done":"waiting");
754
- var current=r.stage==="implementation"?"Codex is editing the isolated workspace.":r.stage==="review"?"Claude is reviewing the work without editing it.":r.stage==="verification"?"Codex is fixing findings and running local checks.":r.status==="running"?"Codex and Claude are discussing. No code is being changed in this stage.":r.status==="open"?(hasVerification?"The room is open. Earlier implementation and review activity is saved below.":"The room is open. No approved coding task is running."):hasVerification?"Local implementation, review, and verification finished.":"No approved coding task is running. Discussion remains separate from implementation.";
755
- var tabs=[["all","All activity"],["code","Code & files"],["tests","Tests & review"],["results","Results"]].map(function(x){return '<button class="'+(tab===x[0]?"on":"")+'" data-rt-work-tab="'+x[0]+'" data-rt-work-run="'+esc(r.id)+'">'+x[1]+'</button>';}).join("");
756
- var items=filtered.length?filtered.map(function(a){var who=a.agent==="room"?"Room":String(a.agent||"room");var detail=String(a.detail||"");return '<article class="rtworkitem '+esc(a.state||"info")+'"><i></i><div><div class="rtworkitemhead"><b>'+esc(a.title||"Work update")+'</b><span>'+esc(who)+' · '+esc(tm(a.updatedAt||a.createdAt))+'</span></div>'+(detail?'<div class="rtworkbrief">'+esc(clip(detail.replace(/[#>*_|\[\]()]/g," ").replace(/\s+/g," "),220))+'</div><details><summary>Open full activity</summary><pre>'+esc(detail)+'</pre></details>':'')+'</div></article>';}).join(""):'<div class="rtworkempty">Nothing in this view yet. It will update automatically when the agents reach this stage.</div>';
757
- return '<section class="rtworkbench"><div class="rtworkhead"><div><span>Live Work Bench</span><b>See what the agents are doing</b></div><small>'+(r.status==="running"||r.status==="executing"?'<i></i> Live':'Saved')+'</small></div><div class="rtworkflow">'+phases+'</div><div class="rtworknow">'+esc(current)+'</div><div class="rtworktabs">'+tabs+'</div><div class="rtworkfeed">'+items+'</div><div class="rtworksafe">Shows visible work, files, tests, reviews, and results. Private reasoning and secrets are never displayed.</div></section>';
758
- }
759
- function roundtableRun(r){
760
- var stage=(r.status==="running"||r.status==="executing")?r.stage:r.status;
761
- var turns=(r.turns||[]).map(function(t){var speaker=t.agent==="user"?"You":t.agent==="joint"?"Codex + Claude · agreed action":t.agent==="coordinator"?"Earlier room summary":t.agent;var content=String(t.content||"");var long=content.length>900;var message=long?'<div class="rtplain">'+esc(clip(content,520))+'</div><details class="rttechnical"><summary>Open the full detailed message</summary><pre>'+esc(content)+'</pre></details>':'<pre>'+esc(content)+'</pre>';return '<article class="rtturn '+esc(t.agent)+'"><div class="rtturnhead"><span>'+esc(speaker)+'</span><small>'+esc(tm(t.createdAt))+'</small></div>'+message+'</article>';}).join("");
762
- if(r.status==="queued"||r.status==="running"||r.status==="executing") turns+='<div class="rtprogress"><i></i><span>'+esc(stageLabel(stage))+'</span></div>';
763
- var canTalk=r.status==="open"||r.status==="awaiting_approval"||r.status==="complete"||r.status==="rejected"||r.status==="failed";
764
- var talkHint=canTalk?"Talk normally. A greeting stays a greeting; a question becomes a real back-and-forth discussion.":"Codex and Claude are replying. You can keep writing here; Send becomes available when this step finishes.";
765
- turns+='<div class="rtjoin"><b>Talk to the Thesis Room</b><span>'+esc(talkHint)+'</span><textarea maxlength="4000" data-rt-message-input="'+esc(r.id)+'" placeholder="Talk to Codex and Claude…">'+esc(rtDrafts[r.id]||"")+'</textarea><div><span class="mono">You · Codex · Claude</span><span class="rtjoinbuttons">'+(r.status==="open"?'<button class="btn ghost" data-rt-continue="'+esc(r.id)+'">Continue debate</button><button class="btn ghost" data-rt-propose="'+esc(r.id)+'">Ask for an agreed action</button>':'')+'<button class="btn" data-rt-message="'+esc(r.id)+'"'+(canTalk?'':' disabled')+'>Send to room</button></span></div></div>';
766
- if(r.status==="awaiting_approval") turns+='<div class="rtapproval"><div><b>Suggested action ready</b><span>You can keep discussing, reject it, or approve implementation and local testing.</span></div><div><button class="btn ghost" data-rt-action="reject" data-rt-id="'+esc(r.id)+'">Reject</button><button class="btn" data-rt-action="approve" data-rt-id="'+esc(r.id)+'">Approve action</button></div></div>';
767
- if(r.status==="complete"){var verified=(r.turns||[]).some(function(t){return t.kind==="verification";});turns+='<div class="rtapproved">'+(verified?'Implementation, independent review, and local verification are complete. External or paid runs remain separately approval-gated.':'The room paused without claiming agreement. Keep talking to resolve the open point; nothing has been approved or implemented.')+'</div>';}
768
- if(r.status==="rejected") turns+='<div class="rtrejected">Rejected · kept in discussion history, not saved as a project decision.</div>';
769
- if(r.error) turns+='<div class="rterror">'+esc(r.error)+'</div>';
770
- var topic=r.mode==="autonomous"?"Agenda selected from Peon memory":r.question;
771
- var quick='<div class="rtlivesummary"><span>Quick summary</span><b>'+esc(r.liveSummary||"A one-sentence summary will appear when the next debate turn arrives.")+'</b><small>This updates automatically. The full conversation stays below if you want the details.</small></div>';
772
- return '<section class="panel rtconversation"><div class="rtchathead"><div><span>Shared room</span><h2>Thesis conversation</h2><small>'+esc(topic)+'</small></div><div class="rtchatpresence"><i class="codex"></i><b>Codex</b><i class="claude"></i><b>Claude</b><span class="rtstate '+esc(r.status)+'">'+esc(stage.replace("_"," "))+'</span></div></div>'+quick+'<div class="rtturns">'+turns+'</div></section>';
773
- }
774
- function wireRoundtableActions(){
775
- Array.prototype.forEach.call(document.querySelectorAll("[data-rt-action]"),function(b){b.addEventListener("click",function(){roundtableDecision(b.getAttribute("data-rt-id"),b.getAttribute("data-rt-action"));});});
776
- Array.prototype.forEach.call(document.querySelectorAll("[data-rt-message]"),function(b){b.addEventListener("click",function(){sendRoundtableMessage(b.getAttribute("data-rt-message"));});});
777
- Array.prototype.forEach.call(document.querySelectorAll("[data-rt-propose]"),function(b){b.addEventListener("click",function(){requestAgreedAction(b.getAttribute("data-rt-propose"));});});
778
- Array.prototype.forEach.call(document.querySelectorAll("[data-rt-continue]"),function(b){b.addEventListener("click",function(){continueRoomDebate(b.getAttribute("data-rt-continue"));});});
779
- Array.prototype.forEach.call(document.querySelectorAll("[data-rt-work-tab]"),function(b){b.addEventListener("click",function(){rtWorkTabs[b.getAttribute("data-rt-work-run")]=b.getAttribute("data-rt-work-tab");renderRoundtablePage();});});
780
- Array.prototype.forEach.call(document.querySelectorAll("[data-rt-message-input]"),function(input){var id=input.getAttribute("data-rt-message-input");input.addEventListener("input",function(){rtDrafts[id]=input.value;});input.addEventListener("keydown",function(e){if((e.metaKey||e.ctrlKey)&&e.key==="Enter")sendRoundtableMessage(id);});});
781
- }
782
- async function roundtableDecision(id,action){
783
- if(rtBusy||!id) return; rtBusy=true;
784
- try{var res=await fetch("/roundtable/runs/"+encodeURIComponent(id)+"/"+action,{method:"POST",headers:{"content-type":"application/json"},body:"{}"});var out=await res.json();if(!res.ok)throw new Error(out.error||"Could not save decision");rtRuns=rtRuns.map(function(r){return r.id===id?out:r;});}
785
- catch(e){var run=rtRuns.find(function(r){return r.id===id;});if(run)run.error=e.message||"Could not save decision";}
786
- rtBusy=false;renderRoundtablePage();
787
- }
788
- async function requestAgreedAction(id){
789
- if(rtBusy||!id)return;rtBusy=true;renderRoundtablePage();
790
- try{var res=await fetch("/roundtable/runs/"+encodeURIComponent(id)+"/propose",{method:"POST",headers:{"content-type":"application/json"},body:"{}"});var out=await res.json();if(!res.ok)throw new Error(out.error||"Could not ask for an agreed action");rtRuns=rtRuns.map(function(r){return r.id===id?out:r;});rtSelected=id;}
791
- catch(e){var run=rtRuns.find(function(r){return r.id===id;});if(run)run.error=e.message||"Could not ask for an agreed action";}
792
- rtBusy=false;renderRoundtablePage();
793
- }
794
- async function continueRoomDebate(id){
795
- if(rtBusy||!id)return;rtBusy=true;renderRoundtablePage();
796
- try{var res=await fetch("/roundtable/runs/"+encodeURIComponent(id)+"/continue",{method:"POST",headers:{"content-type":"application/json"},body:"{}"});var out=await res.json();if(!res.ok)throw new Error(out.error||"Could not continue the debate");rtRuns=rtRuns.map(function(r){return r.id===id?out:r;});rtSelected=id;}
797
- catch(e){var run=rtRuns.find(function(r){return r.id===id;});if(run)run.error=e.message||"Could not continue the debate";}
798
- rtBusy=false;renderRoundtablePage();
799
- }
800
- async function sendRoundtableMessage(id){
801
- if(rtBusy||!id)return;
802
- var input=document.querySelector('[data-rt-message-input="'+CSS.escape(id)+'"]'), message=input?input.value.trim():"";
803
- if(!message)return;
804
- rtDrafts[id]=message;
805
- rtBusy=true;renderRoundtablePage();
806
- try{var res=await fetch("/roundtable/runs/"+encodeURIComponent(id)+"/message",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({message:message})});var out=await res.json();if(!res.ok)throw new Error(out.error||"Could not send message");delete rtDrafts[id];rtRuns=rtRuns.map(function(r){return r.id===id?out:r;});rtSelected=id;rtLoadedAt=0;}
807
- catch(e){var run=rtRuns.find(function(r){return r.id===id;});if(run)run.error=e.message||"Could not send message";}
808
- rtBusy=false;await loadRoundtable(true);
809
- }
810
- function stageLabel(s){ return s==="queued"?"Waiting for the current discussion…":s==="discussion"?"The shared room is active. Each agent sees and answers the full conversation…":s==="proposals"?"Codex and Claude are researching and forming independent proposals…":s==="critiques"?"They are reviewing each other’s proposals…":s==="synthesis"?"The room is forming one joint proposal for your approval…":s==="implementation"?"Approved: Codex is implementing and running local tests…":s==="review"?"Claude is independently reviewing the implementation…":s==="verification"?"Codex is addressing findings and verifying the result…":"Working…"; }
811
- async function startRoundtable(){
812
- if(rtBusy||!ui.project) return;
813
- var q=EL("rt-question"), question=q?q.value.trim():"";
814
- rtBusy=true; renderRoundtablePage();
815
- try{
816
- var body={projectPath:ui.project}; if(question) body.question=question;
817
- var res=await fetch("/roundtable/runs",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify(body)});
818
- var out=await res.json(); if(!res.ok) throw new Error(out.error||"Could not start discussion");
819
- rtSelected=out.id; rtRuns.unshift(out); if(q)q.value=""; rtLoadedAt=0;
820
- }catch(e){ var msg=EL("rt-msg"); if(msg) msg.textContent=e.message||"Could not start discussion"; }
821
- rtBusy=false; await loadRoundtable(true);
822
- }
823
-
824
677
  // ===== OPS (tokens + activity) =====
825
678
  function renderOpsPage(){
826
679
  /* health strip: serve latency/cost (24h telemetry) + STL daily self-check verdict */
@@ -851,7 +704,7 @@ const CLIENT_SCRIPT = String.raw `
851
704
 
852
705
  // wire-up
853
706
  window.addEventListener("hashchange",function(){ renderRoute(true); });
854
- EL("switcher").addEventListener("change",function(e){ ui.project=e.target.value; ui.search=null; ui.focus=null; ui.editing=null; overview=null; rtSelected=null; rtRuns=[]; rtLoadedAt=0; renderRoute(true); });
707
+ EL("switcher").addEventListener("change",function(e){ ui.project=e.target.value; ui.search=null; ui.focus=null; ui.editing=null; overview=null; renderRoute(true); });
855
708
  EL("go").addEventListener("click",runSearch);
856
709
  EL("clear").addEventListener("click",function(){ ui.search=null; EL("q").value=""; EL("clear").hidden=true; renderMemoryPage(); });
857
710
  EL("q").addEventListener("keydown",function(e){ if(e.key==="Enter") runSearch(); });
@@ -868,13 +721,13 @@ const DOCUMENT = String.raw `<!doctype html>
868
721
  <title>PEON // NEURAL HUD</title>
869
722
  <style>
870
723
  :root{
871
- --bg:#0b0d0f; --bg2:#111519; --panel:rgba(22,26,29,.82); --panel2:rgba(17,21,24,.82);
872
- --line:rgba(171,185,178,.16); --line2:rgba(171,185,178,.32);
873
- --ink:#e6ebe7; --muted:#a2aba6; --faint:#6f7773;
874
- --cyan:#87a99a; --cyan-soft:rgba(135,169,154,.11); --cyan-ink:#c4d6ce;
875
- --green:#87b59b; --green-soft:rgba(135,181,155,.11); --green-ink:#c9ded1;
876
- --blue:#9aa9b5; --blue-soft:rgba(154,169,181,.12); --violet:#aaa4bd;
877
- --amber:#d2b878; --amber-soft:rgba(210,184,120,.12); --red:#cc817a;
724
+ --bg:#020710; --bg2:#041020; --panel:rgba(7,22,38,.66); --panel2:rgba(5,16,30,.66);
725
+ --line:rgba(89,227,255,.16); --line2:rgba(89,227,255,.36);
726
+ --ink:#d9f6ff; --muted:#7fa9c0; --faint:#48708a;
727
+ --cyan:#59e3ff; --cyan-soft:rgba(89,227,255,.12); --cyan-ink:#b9f2ff;
728
+ --green:#5dffb0; --green-soft:rgba(93,255,176,.12); --green-ink:#b9ffdd;
729
+ --blue:#7db4ff; --blue-soft:rgba(125,180,255,.14); --violet:#9db4ff;
730
+ --amber:#ffc957; --amber-soft:rgba(255,201,87,.13); --red:#ff7a70;
878
731
  --mono:ui-monospace,SFMono-Regular,"SF Mono",Menlo,monospace; --r:4px;
879
732
  --cham:polygon(10px 0,100% 0,100% calc(100% - 10px),calc(100% - 10px) 100%,0 100%,0 10px);
880
733
  }
@@ -895,30 +748,6 @@ const DOCUMENT = String.raw `<!doctype html>
895
748
  body::after{content:""; position:fixed; left:0; right:0; top:-15%; height:26vh; z-index:98; pointer-events:none;
896
749
  background:linear-gradient(180deg, transparent, rgba(89,227,255,.045) 55%, rgba(89,227,255,.09) 60%, transparent);
897
750
  animation:scanroam 9s linear infinite;}
898
- body.room-theme{
899
- --bg:#1d1d1a;--bg2:#24231f;--panel:rgba(42,41,36,.96);--panel2:rgba(35,34,30,.96);
900
- --line:rgba(226,218,196,.13);--line2:rgba(226,218,196,.28);
901
- --ink:#f0ede3;--muted:#bbb4a4;--faint:#888174;
902
- --cyan:#9aaa8d;--cyan-soft:rgba(154,170,141,.12);--cyan-ink:#d7ddcf;
903
- --green:#9aaa8d;--green-soft:rgba(154,170,141,.12);--green-ink:#d7ddcf;
904
- --blue:#a69d8c;--blue-soft:rgba(166,157,140,.12);--violet:#b7a8bd;
905
- --amber:#c5a66f;--amber-soft:rgba(197,166,111,.12);--red:#c58478;
906
- background:
907
- radial-gradient(900px 560px at 50% -180px,rgba(154,170,141,.10),transparent 68%),
908
- linear-gradient(rgba(226,218,196,.018) 1px,transparent 1px),
909
- linear-gradient(90deg,rgba(226,218,196,.018) 1px,transparent 1px),#1d1d1a;
910
- background-size:auto,48px 48px,48px 48px,auto;
911
- }
912
- body.room-theme::before{background:repeating-linear-gradient(0deg,rgba(244,237,218,.012) 0 1px,transparent 1px 4px)}
913
- body.room-theme::after{display:none}
914
- body.room-theme header{background:rgba(29,29,26,.96);box-shadow:0 12px 34px -24px rgba(0,0,0,.8)}
915
- body.room-theme header::after{background:linear-gradient(90deg,transparent,rgba(154,170,141,.55),transparent);animation:none}
916
- body.room-theme .logo{background:radial-gradient(circle at 44% 38%,#f1eee3,#aeb9a4 42%,#68745f 72%,#30352c);color:#252820;box-shadow:none}
917
- body.room-theme .logo::before,body.room-theme .logo::after{border-color:rgba(180,192,168,.35);animation:none}
918
- body.room-theme nav a{background:rgba(226,218,196,.03)}
919
- body.room-theme nav a:hover{background:rgba(154,170,141,.10)}
920
- body.room-theme nav a.on{background:rgba(154,170,141,.16);color:#f0ede3;box-shadow:none;text-shadow:none}
921
- body.room-theme .switcher{background:#272620}
922
751
  @keyframes scanroam{0%{transform:translateY(-30vh);} 100%{transform:translateY(160vh);}}
923
752
  h1,h2,h3,p{margin:0;}
924
753
  .mono{font-family:var(--mono); font-size:11px; letter-spacing:.02em;}
@@ -1038,9 +867,9 @@ const DOCUMENT = String.raw `<!doctype html>
1038
867
  .revacts{flex:none; display:flex; gap:6px;}
1039
868
 
1040
869
  /* buttons / inputs */
1041
- .btn{padding:8px 16px; clip-path:var(--cham); background:linear-gradient(180deg, rgba(135,169,154,.92), rgba(104,142,127,.92));
1042
- color:#101713; font-weight:760; font-size:11.5px; letter-spacing:.1em; text-transform:uppercase; border:0; transition:all .15s;
1043
- box-shadow:0 0 14px -7px rgba(135,169,154,.55);}
870
+ .btn{padding:8px 16px; clip-path:var(--cham); background:linear-gradient(180deg, rgba(89,227,255,.9), rgba(43,168,201,.9));
871
+ color:#03202c; font-weight:760; font-size:11.5px; letter-spacing:.1em; text-transform:uppercase; border:0; transition:all .15s;
872
+ box-shadow:0 0 18px -6px rgba(89,227,255,.8);}
1044
873
  .btn:hover{filter:brightness(1.12);} .btn:disabled{opacity:.45; cursor:not-allowed;}
1045
874
  .btn.ghost{background:rgba(89,227,255,.06); color:var(--cyan-ink); border:1px solid var(--line2); box-shadow:none;}
1046
875
  .btn.ghost:hover{background:rgba(89,227,255,.13);}
@@ -1102,37 +931,6 @@ const DOCUMENT = String.raw `<!doctype html>
1102
931
  .pcgo{font-size:10px; letter-spacing:.14em; text-transform:uppercase; color:var(--cyan); margin-top:10px; opacity:0; transition:opacity .15s; text-shadow:0 0 8px rgba(89,227,255,.6);}
1103
932
  .pcard:hover .pcgo{opacity:1;}
1104
933
 
1105
- /* thesis studio */
1106
- .rtstudio{display:grid;grid-template-columns:250px minmax(520px,1fr) 360px;gap:14px;align-items:start;max-width:1640px;margin:0 auto}
1107
- .rtstudio-main{min-width:0}.rtstudio-nav,.rtstudio-work{display:flex;flex-direction:column;gap:12px;min-width:0}.rtstudio-nav,.rtstudio-work{position:sticky;top:74px;max-height:calc(100vh - 92px);overflow:auto;padding-right:2px}
1108
- .rtstudio-card{border:1px solid rgba(227,220,202,.12);border-radius:16px;background:rgba(38,37,32,.92);box-shadow:0 14px 32px -28px #000}
1109
- .rtstudio-label,.rtstudio-sectionhead span,.rtcheckpoint>span{font:700 9px var(--mono);letter-spacing:.15em;text-transform:uppercase;color:#b8a578}
1110
- .rtgoal{display:flex;flex-direction:column;gap:4px;padding:17px;background:linear-gradient(145deg,rgba(154,170,141,.13),rgba(197,166,111,.07))}.rtgoal b{font-size:23px;color:#f3efe4;letter-spacing:-.02em}.rtgoal strong{font:10px var(--mono);letter-spacing:.09em;color:#b9c6ae}.rtgoal small{margin-top:7px;color:var(--muted);font-size:11px;line-height:1.5}
1111
- .rtteam{padding:14px}.rtteammate{display:grid;grid-template-columns:34px 1fr;gap:9px;align-items:center;padding:11px 0;border-bottom:1px solid rgba(255,255,255,.055)}.rtteammate:last-of-type{border-bottom:0}.rtteammate p{margin:4px 0 0;color:var(--faint);font-size:10px;line-height:1.4}.rtteam>small{display:block;margin-top:10px;color:var(--faint);font:9px/1.45 var(--mono)}
1112
- .rtminiavatar{width:32px;height:32px;border-radius:10px;display:grid;place-items:center;background:#34372f;border:1px solid rgba(154,170,141,.35);color:#e9e5da;font:700 13px ui-sans-serif}.rtteammate.claude .rtminiavatar{background:#39333b;border-color:rgba(183,168,189,.38)}
1113
- .rtagent{display:grid;grid-template-columns:8px auto;column-gap:7px;align-items:center}.rtagent i{width:7px;height:7px;border-radius:50%;grid-row:1/3;background:var(--faint)}.rtagent.ok i{background:var(--green);box-shadow:0 0 8px rgba(93,255,176,.65)}.rtagent.bad i{background:var(--red)}.rtagent b{font-size:11px;color:var(--ink)}.rtagent small{font:8.5px var(--mono);color:var(--faint)}
1114
- .rtcheckpoint{padding:14px}.rtcheckpoint b{display:block;margin:8px 0;color:#dfdbcf;font-size:11.5px;line-height:1.5}.rtcheckpoint small{color:#9aaa8d;font:9px var(--mono);text-transform:uppercase;letter-spacing:.08em}
1115
- .rtstudio-new{overflow:hidden}.rtstudio-new>summary{padding:13px 14px;color:#ddd5c3;font:700 10px var(--mono);letter-spacing:.08em;text-transform:uppercase;cursor:pointer}.rtnewbody{padding:0 14px 14px}.rtnewbody textarea{width:100%;min-height:78px;resize:vertical;padding:10px 11px;border:1px solid rgba(227,220,202,.13);border-radius:10px;background:#24231f;color:var(--ink);font:12px/1.5 ui-sans-serif}.rtnewbody textarea:focus{outline:none;border-color:#9aaa8d;box-shadow:0 0 0 3px rgba(154,170,141,.09)}
1116
- .rtactions{display:flex;flex-direction:column;align-items:stretch;gap:8px;margin-top:9px}.rtactions .mono{color:var(--faint);font-size:8.5px}.rtautodesc{padding:9px 10px;border-left:2px solid #9aaa8d;background:rgba(154,170,141,.055);color:var(--muted);line-height:1.5;margin-bottom:9px;border-radius:0 8px 8px 0;font-size:10.5px}
1117
- .rtstudio-history{padding:13px}.rtstudio-sectionhead{display:flex;align-items:center;justify-content:space-between;margin-bottom:9px}.rtstudio-sectionhead small{font:8px var(--mono);color:var(--faint)}.rthistory{display:flex;flex-direction:column;gap:5px}.rthist{display:flex;justify-content:space-between;align-items:center;gap:8px;text-align:left;padding:8px 9px;border:1px solid rgba(227,220,202,.09);border-radius:9px;background:#292823;color:var(--muted)}.rthist:hover,.rthist.on{border-color:rgba(154,170,141,.48);color:var(--ink);background:rgba(154,170,141,.08)}.rthist span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:10px}
1118
- .rtstate{font:8px var(--mono);letter-spacing:.1em;text-transform:uppercase;padding:3px 7px;border-radius:999px;background:rgba(255,255,255,.06);color:var(--muted);white-space:nowrap}.rtstate.complete{background:var(--green-soft);color:var(--green-ink)}.rtstate.failed,.rtstate.rejected{background:rgba(255,122,112,.12);color:var(--red)}.rtstate.running,.rtstate.queued,.rtstate.executing{background:rgba(154,170,141,.14);color:#d7ddcf}.rtstate.awaiting_approval{background:var(--amber-soft);color:var(--amber)}.rtstate.open{background:rgba(154,170,141,.14);color:#d7ddcf}
1119
- .rtconversation{border-radius:20px!important;background:rgba(35,34,30,.94)!important;padding:0!important;overflow:hidden;border-color:rgba(227,220,202,.13)!important}
1120
- .rtchathead{display:flex;justify-content:space-between;align-items:center;gap:16px;padding:16px 18px;border-bottom:1px solid rgba(227,220,202,.1);background:#292823}.rtchathead>div:first-child{min-width:0}.rtchathead span:first-child{color:#b8a578;font:700 8.5px var(--mono);letter-spacing:.15em;text-transform:uppercase}.rtchathead h2{margin:3px 0 2px;font-size:17px;color:#f0ece2}.rtchathead small{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--faint);font-size:10px}.rtchatpresence{display:flex;align-items:center;gap:5px;flex:none}.rtchatpresence i{width:7px;height:7px;border-radius:50%}.rtchatpresence i.codex{background:#9aaa8d}.rtchatpresence i.claude{background:#b7a8bd;margin-left:6px}.rtchatpresence b{font:8.5px var(--mono);color:var(--muted)}.rtchatpresence .rtstate{margin-left:8px}
1121
- .rtlivesummary{display:flex;flex-direction:column;gap:4px;margin:12px 14px 0;padding:12px 14px;border:1px solid rgba(197,166,111,.22);border-radius:12px;background:#302d25}.rtlivesummary span{color:#c5a66f;font:700 8.5px var(--mono);letter-spacing:.13em;text-transform:uppercase}.rtlivesummary b{color:#ebe6dc;font-size:12px;line-height:1.45}.rtlivesummary small{color:var(--faint);font-size:9.5px}
1122
- .rtturns{display:flex;flex-direction:column;gap:12px;height:calc(100vh - 255px);min-height:560px;overflow-y:auto;padding:15px 15px 0;scroll-behavior:smooth}.rtturn{position:relative;flex:none;max-width:86%;border:1px solid rgba(227,220,202,.11);background:#2b2a25;border-radius:15px;overflow:hidden;box-shadow:0 9px 20px -20px #000}.rtturn.codex{align-self:flex-start;border-left:3px solid #9aaa8d;border-bottom-left-radius:4px}.rtturn.claude{align-self:flex-end;border-right:3px solid #b7a8bd;border-bottom-right-radius:4px}.rtturn.user{align-self:center;max-width:78%;border:1px solid rgba(197,166,111,.34);background:#332f26}.rtturn.joint,.rtturn.coordinator{align-self:center;max-width:92%;border:1px solid rgba(154,170,141,.34);background:#2c3029}
1123
- .rtturnhead{display:flex;justify-content:space-between;gap:10px;padding:8px 11px;border-bottom:1px solid rgba(227,220,202,.08);font:700 9px var(--mono);letter-spacing:.09em;text-transform:uppercase;color:#bac7af}.rtturn.claude .rtturnhead{color:#c8bed0}.rtturn.user .rtturnhead{color:#d8bd86}.rtturn.joint .rtturnhead,.rtturn.coordinator .rtturnhead{color:var(--green-ink)}.rtturnhead small{color:var(--faint);font-size:8px}
1124
- .rtturn pre,.rtplain{margin:0;padding:11px 13px;white-space:pre-wrap;overflow-wrap:anywhere;color:var(--ink);font:12.5px/1.58 ui-sans-serif,system-ui}.rttechnical{margin:0 11px 11px;border-top:1px solid rgba(227,220,202,.09);padding-top:8px}.rttechnical summary{color:var(--muted);font-size:10px;cursor:pointer}.rttechnical pre{margin-top:7px;padding:10px;background:#23221e;border-radius:9px;color:#c8c2b4;font:10px/1.5 var(--mono)}
1125
- .rtprogress{display:flex;align-items:center;gap:9px;padding:11px;color:var(--muted);font-family:var(--mono)}.rtprogress i{width:8px;height:8px;border-radius:50%;background:#9aaa8d;box-shadow:0 0 9px rgba(154,170,141,.8);animation:beat 1.1s infinite}.rterror{padding:11px 13px;color:#ffb0a8;background:rgba(255,122,112,.08);border:1px solid rgba(255,122,112,.24);font-family:var(--mono)}
1126
- .rtapproval{display:flex;align-items:center;justify-content:space-between;gap:14px;padding:13px 14px;border:1px solid rgba(255,201,87,.34);background:var(--amber-soft);border-radius:12px}.rtapproval>div:first-child{display:flex;flex-direction:column}.rtapproval b{color:var(--amber);font-size:12px}.rtapproval span{color:var(--muted);font-size:11px}.rtapproval>div:last-child{display:flex;gap:8px;flex:none}
1127
- .rtjoin{position:sticky;bottom:0;z-index:7;display:flex;flex:none;flex-direction:column;gap:7px;margin:4px -15px 0;padding:13px 15px 15px;border-top:1px solid rgba(197,166,111,.25);background:linear-gradient(180deg,rgba(43,41,35,.97),#2b2923);box-shadow:0 -18px 32px -30px #000}.rtjoin>b{color:#ead7ae;font-size:12px}.rtjoin>span{color:var(--muted);font-size:10px}.rtjoin textarea{width:100%;min-height:68px;max-height:180px;resize:vertical;padding:10px 12px;border:1px solid rgba(227,220,202,.15);border-radius:11px;background:#211f1b;color:var(--ink);font:12.5px/1.5 ui-sans-serif}.rtjoin textarea:focus{outline:none;border-color:#c5a66f;box-shadow:0 0 0 3px rgba(197,166,111,.08)}.rtjoin>div{display:flex;align-items:center;justify-content:space-between;gap:10px}.rtjoin .mono{color:var(--faint)}.rtjoinbuttons{display:flex;gap:7px;align-items:center;flex-wrap:wrap}
1128
- .rtapproved,.rtrejected{padding:10px 12px;font:10px var(--mono);border-radius:10px}.rtapproved{color:var(--green-ink);background:var(--green-soft);border:1px solid rgba(93,255,176,.25)}.rtrejected{color:#ffb0a8;background:rgba(255,122,112,.08);border:1px solid rgba(255,122,112,.24)}
1129
- .rtstudio-work .rtworkbench{margin:0;padding:15px;border:1px solid rgba(227,220,202,.13);border-radius:17px;background:#282823;box-shadow:0 14px 32px -28px #000}.rtworkhead{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:13px}.rtworkhead>div{display:flex;flex-direction:column;gap:3px}.rtworkhead span{color:#aab99d;font:700 9px var(--mono);letter-spacing:.14em;text-transform:uppercase}.rtworkhead b{color:#f0ede3;font-size:14px}.rtworkhead small{display:flex;align-items:center;gap:7px;color:#cbd4c4;font:9px var(--mono);text-transform:uppercase;letter-spacing:.1em}.rtworkhead small i{width:7px;height:7px;border-radius:50%;background:#9aaa8d;box-shadow:0 0 8px rgba(154,170,141,.75);animation:beat 1.1s infinite}
1130
- .rtworkflow{display:grid;grid-template-columns:repeat(4,1fr);gap:5px;margin-bottom:9px}.rtworkphase{display:flex;align-items:center;gap:5px;padding:7px;border:1px solid rgba(227,220,202,.1);border-radius:9px;color:var(--faint);font:8.5px var(--mono)}.rtworkphase i{width:6px;height:6px;border-radius:50%;background:#5b5a52}.rtworkphase.done{color:#cbd4c4;background:rgba(154,170,141,.07)}.rtworkphase.done i{background:#9aaa8d}.rtworkphase.active{color:#ead7ae;border-color:rgba(197,166,111,.36);background:rgba(197,166,111,.08)}.rtworkphase.active i{background:#c5a66f;box-shadow:0 0 8px rgba(197,166,111,.7);animation:beat 1.1s infinite}
1131
- .rtworknow{padding:9px 10px;margin-bottom:9px;border-left:3px solid #9aaa8d;border-radius:0 8px 8px 0;background:rgba(154,170,141,.07);color:#d9d6cc;font-size:11px;line-height:1.45}.rtworktabs{display:flex;gap:5px;overflow:auto;margin-bottom:9px}.rtworktabs button{border:1px solid rgba(227,220,202,.1);border-radius:999px;background:#22221e;color:var(--muted);padding:5px 8px;font:8.5px var(--mono);white-space:nowrap}.rtworktabs button:hover,.rtworktabs button.on{border-color:rgba(197,166,111,.45);color:#f0e5cd;background:rgba(197,166,111,.09)}
1132
- .rtworkfeed{display:flex;flex-direction:column;gap:7px;overflow:auto;padding-right:2px}.rtworkitem{display:grid;grid-template-columns:8px 1fr;gap:8px;padding:9px;border:1px solid rgba(255,255,255,.055);border-radius:10px;background:#23231f}.rtworkitem>i{width:7px;height:7px;border-radius:50%;margin-top:4px;background:#85847a}.rtworkitem.running>i{background:#c5a66f;box-shadow:0 0 8px rgba(197,166,111,.65);animation:beat 1.1s infinite}.rtworkitem.passed>i{background:#9aaa8d}.rtworkitem.failed>i{background:#d78a82}.rtworkitemhead{display:flex;align-items:baseline;justify-content:space-between;gap:8px}.rtworkitemhead b{font-size:10.5px;color:#ebe7dd}.rtworkitemhead span{font:8px var(--mono);color:var(--faint);text-transform:capitalize}.rtworkbrief{margin-top:4px;color:var(--muted);font-size:10px;line-height:1.4}.rtworkitem details{margin-top:6px}.rtworkitem summary{color:#bdb5a5;font-size:9px;cursor:pointer}.rtworkitem pre{max-height:250px;overflow:auto;margin:7px 0 0;padding:9px;border-radius:8px;background:#1f1f1b;color:#c8c2b4;white-space:pre-wrap;font:9.5px/1.5 var(--mono)}.rtworkempty{padding:14px;text-align:center;color:var(--faint);font-size:10px;border:1px dashed rgba(227,220,202,.12);border-radius:9px}.rtworksafe{margin-top:9px;color:var(--faint);font:8.5px/1.4 var(--mono)}
1133
- @media(max-width:1280px){.rtstudio{grid-template-columns:220px minmax(480px,1fr)}.rtstudio-work{position:static;grid-column:1/-1;max-height:none}.rtstudio-work .rtworkfeed{max-height:300px}}
1134
- @media(max-width:820px){.rtstudio{grid-template-columns:1fr}.rtstudio-nav,.rtstudio-work{position:static;max-height:none}.rtstudio-main{grid-row:1}.rtstudio-nav{grid-row:2}.rtstudio-work{grid-row:3}.rtturns{height:auto;min-height:520px;max-height:75vh}.rtturn{max-width:96%}.rtturn.user{max-width:92%}.rtchathead{align-items:flex-start;flex-direction:column}.rtjoin>div,.rtapproval{align-items:stretch;flex-direction:column}.rtworkflow{grid-template-columns:1fr 1fr}.rtworkitemhead{align-items:flex-start;flex-direction:column;gap:2px}}
1135
-
1136
934
  /* ops */
1137
935
  .bignums{display:grid; grid-template-columns:repeat(4,1fr); gap:14px; margin-bottom:16px;}
1138
936
  .bignum{padding:15px 17px;}
@@ -1266,7 +1064,6 @@ const DOCUMENT = String.raw `<!doctype html>
1266
1064
  <nav>
1267
1065
  <a id="nav-brain" href="#/brain">Neural Core</a>
1268
1066
  <a id="nav-projects" href="#/projects">Sectors</a>
1269
- <a id="nav-roundtable" href="#/roundtable">Thesis Room</a>
1270
1067
  <a id="nav-ops" href="#/ops">Systems</a>
1271
1068
  </nav>
1272
1069
  <select class="switcher" id="switcher" aria-label="Active project"></select>
@@ -1333,13 +1130,6 @@ const DOCUMENT = String.raw `<!doctype html>
1333
1130
  </div>
1334
1131
  </section>
1335
1132
 
1336
- <!-- SYSTEMS (ops) -->
1337
- <section id="page-roundtable" hidden>
1338
- <div class="pagehead">Thesis Room</div>
1339
- <div class="pagesub">You, Codex, and Claude share one workspace for ongoing thesis research, discussion, decisions, and carefully approved experiments.</div>
1340
- <div id="rt-body"><div class="empty">Connecting the two agents…</div></div>
1341
- </section>
1342
-
1343
1133
  <!-- SYSTEMS (ops) -->
1344
1134
  <section id="page-ops" hidden>
1345
1135
  <div class="pagehead">Systems</div>
@@ -34,14 +34,26 @@ export interface DuplicatePair {
34
34
  bContent: string;
35
35
  similarity: number;
36
36
  }
37
- /**
38
- * Flag near-duplicate ACTIVE beliefs of the same type — a nudge for the user to
39
- * merge, not an automatic action. Conservative threshold to avoid false alarms.
40
- */
41
- export declare function detectDuplicates(records: readonly MemoryRecord[], options?: {
37
+ /** How many record pairs the last duplicate scan actually compared. */
38
+ export declare function duplicateScanStats(): {
39
+ pairsEvaluated: number;
40
+ };
41
+ export interface DetectDuplicatesOptions {
42
42
  threshold?: number;
43
43
  limit?: number;
44
- }): DuplicatePair[];
44
+ /** Compare every pair (the original scan). Kept for equivalence testing. */
45
+ exhaustive?: boolean;
46
+ maxTokenBucket?: number;
47
+ }
48
+ /**
49
+ * Near-duplicate belief pairs, strongest first.
50
+ *
51
+ * Previously this compared every active pair — ~20.4M jaccard computations on a
52
+ * 6.4k-active brain, to return the top 5 — on both the /overview endpoint and the
53
+ * consolidation auto-merge path. Candidates are now found through an inverted index
54
+ * on rare tokens, so the overwhelming majority of pairs are never scored.
55
+ */
56
+ export declare function detectDuplicates(records: readonly MemoryRecord[], options?: DetectDuplicatesOptions): DuplicatePair[];
45
57
  export interface TokenSavings {
46
58
  onAvg: number;
47
59
  offAvg: number;
package/dist/overview.js CHANGED
@@ -50,29 +50,107 @@ function jaccard(a, b) {
50
50
  * Flag near-duplicate ACTIVE beliefs of the same type — a nudge for the user to
51
51
  * merge, not an automatic action. Conservative threshold to avoid false alarms.
52
52
  */
53
+ /**
54
+ * Only the rarest few tokens of a record are used as blocking keys: a pair at
55
+ * jaccard >= 0.6 shares most of its words, so it almost certainly shares one of
56
+ * them. Tokens common to a huge share of the brain are useless as keys and are
57
+ * skipped rather than producing a quadratic bucket.
58
+ */
59
+ const DUPLICATE_BLOCKING_TOKENS = 12;
60
+ const MAX_TOKEN_BUCKET = Number(process.env.PEON_DUPLICATE_MAX_TOKEN_BUCKET) > 0
61
+ ? Number(process.env.PEON_DUPLICATE_MAX_TOKEN_BUCKET)
62
+ : 400;
63
+ let lastDuplicatePairsEvaluated = 0;
64
+ /** How many record pairs the last duplicate scan actually compared. */
65
+ export function duplicateScanStats() {
66
+ return { pairsEvaluated: lastDuplicatePairsEvaluated };
67
+ }
68
+ /**
69
+ * Near-duplicate belief pairs, strongest first.
70
+ *
71
+ * Previously this compared every active pair — ~20.4M jaccard computations on a
72
+ * 6.4k-active brain, to return the top 5 — on both the /overview endpoint and the
73
+ * consolidation auto-merge path. Candidates are now found through an inverted index
74
+ * on rare tokens, so the overwhelming majority of pairs are never scored.
75
+ */
53
76
  export function detectDuplicates(records, options = {}) {
54
77
  const threshold = options.threshold ?? 0.6;
55
78
  const limit = options.limit ?? 5;
56
79
  const active = records.filter((record) => record.status === "active");
57
80
  const sets = active.map((record) => wordSet(record.content));
58
- const pairs = [];
59
- for (let i = 0; i < active.length; i += 1) {
60
- for (let j = i + 1; j < active.length; j += 1) {
61
- if (active[i].type !== active[j].type)
81
+ const found = [];
82
+ let pairsEvaluated = 0;
83
+ const score = (i, j) => {
84
+ if (active[i].type !== active[j].type)
85
+ return;
86
+ pairsEvaluated += 1;
87
+ const similarity = jaccard(sets[i], sets[j]);
88
+ if (similarity < threshold)
89
+ return;
90
+ found.push({
91
+ i,
92
+ j,
93
+ pair: {
94
+ aId: active[i].id,
95
+ aContent: active[i].content,
96
+ bId: active[j].id,
97
+ bContent: active[j].content,
98
+ similarity: Math.round(similarity * 100) / 100
99
+ }
100
+ });
101
+ };
102
+ if (options.exhaustive) {
103
+ for (let i = 0; i < active.length; i += 1) {
104
+ for (let j = i + 1; j < active.length; j += 1)
105
+ score(i, j);
106
+ }
107
+ }
108
+ else {
109
+ const documentFrequency = new Map();
110
+ for (const set of sets) {
111
+ for (const token of set)
112
+ documentFrequency.set(token, (documentFrequency.get(token) ?? 0) + 1);
113
+ }
114
+ const buckets = new Map();
115
+ for (let i = 0; i < active.length; i += 1) {
116
+ const rarest = [...sets[i]]
117
+ .sort((a, b) => (documentFrequency.get(a) ?? 0) - (documentFrequency.get(b) ?? 0))
118
+ .slice(0, DUPLICATE_BLOCKING_TOKENS);
119
+ for (const token of rarest) {
120
+ const bucket = buckets.get(token);
121
+ if (bucket)
122
+ bucket.push(i);
123
+ else
124
+ buckets.set(token, [i]);
125
+ }
126
+ }
127
+ const cap = options.maxTokenBucket ?? MAX_TOKEN_BUCKET;
128
+ const seen = new Set();
129
+ const width = active.length;
130
+ for (const bucket of buckets.values()) {
131
+ if (bucket.length > cap)
62
132
  continue;
63
- const similarity = jaccard(sets[i], sets[j]);
64
- if (similarity >= threshold) {
65
- pairs.push({
66
- aId: active[i].id,
67
- aContent: active[i].content,
68
- bId: active[j].id,
69
- bContent: active[j].content,
70
- similarity: Math.round(similarity * 100) / 100
71
- });
133
+ for (let a = 0; a < bucket.length; a += 1) {
134
+ for (let b = a + 1; b < bucket.length; b += 1) {
135
+ const i = Math.min(bucket[a], bucket[b]);
136
+ const j = Math.max(bucket[a], bucket[b]);
137
+ const key = i * width + j;
138
+ if (seen.has(key))
139
+ continue;
140
+ seen.add(key);
141
+ score(i, j);
142
+ }
72
143
  }
73
144
  }
145
+ // Match the original emission order before the (stable) similarity sort, so
146
+ // equal-similarity pairs come out in the same order as the exhaustive scan.
147
+ found.sort((x, y) => x.i - y.i || x.j - y.j);
74
148
  }
75
- return pairs.sort((left, right) => right.similarity - left.similarity).slice(0, limit);
149
+ lastDuplicatePairsEvaluated = pairsEvaluated;
150
+ return found
151
+ .map((entry) => entry.pair)
152
+ .sort((left, right) => right.similarity - left.similarity)
153
+ .slice(0, limit);
76
154
  }
77
155
  /**
78
156
  * Compare Peon-on vs Peon-off session token totals for a project. Returns null
@@ -88,3 +88,39 @@ export declare class OpenRouterMemoryModelClient implements MemoryModelClient {
88
88
  }): Promise<MemoryModelResult>;
89
89
  }
90
90
  export declare function parseProcessedMemory(content: string): ProcessedMemory;
91
+ /**
92
+ * Did the model server silently truncate the prompt?
93
+ *
94
+ * OpenAI-compatible servers report usage.prompt_tokens: what the model actually read.
95
+ * Ollama, at its 4096-token default, reports exactly 4096 for a ~17K-token prompt.
96
+ *
97
+ * The estimate comes from estimatePromptTokensForTruncation, and the threshold has to
98
+ * respect how rough it is. For English that estimate is chars/4, which OVER-estimates
99
+ * (real text runs ~5.5 chars/token), so an untruncated prompt still reports ~0.73 of
100
+ * it. A truncated one reports ~0.24. Below 0.5 is unambiguous: reaching it without
101
+ * truncation would take 8+ chars per token. Small prompts are ignored, where
102
+ * estimation noise is a large share of the total.
103
+ */
104
+ export declare function detectPromptTruncation(estimatedPromptTokens: number, reportedPromptTokens: number | undefined): boolean;
105
+ /**
106
+ * Prompt size estimate for detectPromptTruncation ONLY. estimateTokens (chars/4) stays
107
+ * the cost/reporting estimate; this one exists because chars/4 undercounts token-dense
108
+ * scripts, which let a truncated CJK prompt pass as untruncated.
109
+ *
110
+ * The detector flags reported/estimated < 0.5, so each weight has to sit between two
111
+ * limits: high enough that a truncated prompt falls below 0.5, and at most ~2x the
112
+ * MOST efficient tokenizer's rate, or an untruncated prompt falls below 0.5 too. That
113
+ * false positive is the worse failure: the session is refused on every retry.
114
+ *
115
+ * - ASCII, 1/4 per char: unchanged, so English behaves exactly as before (chars/4
116
+ * over-counts English ~1.4x; measured untruncated ratio 0.73, truncated 0.24).
117
+ * - Han, kana, hangul, bopomofo, CJK and fullwidth punctuation, 0.75 per char: efficient
118
+ * tokenizers run ~0.45-0.6 tokens per CJK char, giving an untruncated ratio of 0.6-0.8.
119
+ * Qwen2.5 on Ollama runs ~0.65, so a truncated CJK prompt now reads well under 0.5.
120
+ * - Any other non-ASCII, 0.35 per char: Cyrillic, Greek, Arabic, accented Latin and the
121
+ * like pack into ~0.22-0.35 tokens per char on large-vocabulary tokenizers. A flat 0.75
122
+ * here would read an untruncated Russian log at ~0.31 and block it forever.
123
+ *
124
+ * Iterates code points, so an astral character (CJK Extension B, emoji) counts once.
125
+ */
126
+ export declare function estimatePromptTokensForTruncation(text: string): number;