symbiot 0.54.0 → 0.54.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  What each Symbiot release brought, newest first. When Approve bumps the version, it writes that release here from the tasks it approved, worded as release notes by your connected AI (or in the tasks' own words without one). The app shows the releases newer than yours under "What's new" when it offers an update, and once more after you update.
4
4
 
5
+ ## 0.54.2
6
+
7
+ - **Lighter without a graphics card.** When the liquid is drawn by the CPU (no GPU, or a headless browser like the ones agents check Symbiot's screens in), it used 6 cores or more. Now it draws at a third of the resolution, without the breathing, and only when something actually moved, at most 20 times a second: about a third of the CPU. It does the same when your system asks for less motion. With a graphics card, Home also draws half as often at rest and a sixth as often behind an open panel, where it barely shows.
8
+
9
+ ## 0.54.1
10
+
11
+ - **No more lag from a big agent log.** An agent's log keeps every run and everything it reads, screenshots included; one reached 144 MB, and Symbiot reread all of it on almost every refresh, so the app stalled for 5–28 seconds at a time and ate a core. Now it reads only the newest run, from the end of the file, and keeps it until the file changes. A log past 8 MB starts afresh at the next run, with the old one kept as `agent.log.old`.
12
+
5
13
  ## 0.54.0
6
14
 
7
15
  - **Show Symbiot a screenshot.** Paste one into "Talk to Symbiot" (Ctrl+V) or drop it on the bar: it shows as a thumbnail you can remove, goes with what you say, and the AI you connected sees it (Claude, OpenAI, Gemini, or a local model that reads images). If Symbiot hands the work to an agent, the agent gets the screenshot too. Up to 4 at a time; they're kept a week, readable by you only.
package/agents.mjs CHANGED
@@ -5,10 +5,10 @@ import { spawn } from "node:child_process";
5
5
  import { homedir } from "node:os";
6
6
  import { join, dirname, resolve, relative } from "node:path";
7
7
  import { fileURLToPath } from "node:url";
8
- import { readFileSync, writeFileSync, mkdirSync, existsSync, openSync, writeSync, unlinkSync, readdirSync, statSync } from "node:fs";
8
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, openSync, writeSync, unlinkSync, readdirSync, statSync, renameSync } from "node:fs";
9
9
  import { randomBytes, createHash } from "node:crypto";
10
10
  import { CONFIG_DIR, loadConfig, saveConfig, loadTasks, TASK_MAX, clipWords, sameTask, taskWords, sh, hasCmd } from "./core.mjs";
11
- import { parseRun, lastRunText } from "./work.mjs";
11
+ import { parseRun, lastRunText, readRunLog } from "./work.mjs";
12
12
  import { parseFacts } from "./handover.mjs";
13
13
  import { scanRoots, repoPathMap } from "./scan.mjs";
14
14
  import { knowledgeFolders } from "./knowledge.mjs";
@@ -273,7 +273,7 @@ function earlierRuns() {
273
273
  const busy = runningHandoff(r.path), ask = agentQuestions(r.path, r.name), wait = waitingFor(r.path), facts = busy ? [] : factsOf(r.path);
274
274
  if (!busy && !ask.questions.length && !wait && !facts.length) continue;
275
275
  const log = join(r.path, ".symbiot", "agent.log"), startedAt = (busy && busy.startedAt) || Number(r.startedAt) || Date.now();
276
- let tail = "", work = null, progress = null, end = Date.now(); try { ({ tail, work, progress } = workOf(r.path, readFileSync(log, "utf8"))); if (!busy) end = statSync(log).mtimeMs; } catch {}
276
+ let tail = "", work = null, progress = null, end = Date.now(); try { ({ tail, work, progress } = workOf(r.path, readRunLog(log))); if (!busy) end = statSync(log).mtimeMs; } catch {}
277
277
  out.push({ id: "earlier-" + digest(r.path).slice(0, 8), name: String(r.name || r.path.split("/").pop()), path: r.path, status: busy ? "running" : "done", earlier: true, elapsed: Math.max(0, end - startedAt), exitCode: null, tail, work, progress, changed: agentChanges(r.path, startedAt), ask, held: heldTasks(r.path), waiting: wait, remember: facts.length ? facts : null, fromHeld: false });
278
278
  }
279
279
  return out;
@@ -576,10 +576,12 @@ function _findOrcaCli() {
576
576
  // ---- background jobs --------------------------------------------------------
577
577
  // Run a shell command as a tracked, logged background job that shows up live in
578
578
  // the Agents tab. Shared by the agent handoff and the local-model setup.
579
+ const LOG_KEEP = 8 * 1024 * 1024;
579
580
  function track(name, cmd, cwd, onExit, env) {
580
581
  try {
581
582
  const dir = join(cwd, ".symbiot"); mkdirSync(dir, { recursive: true });
582
583
  const logp = join(dir, "agent.log");
584
+ try { if (statSync(logp).size > LOG_KEEP) renameSync(logp, logp + ".old"); } catch {} // past 8 MB: kept as agent.log.old, and a fresh one starts
583
585
  let fd = "ignore"; try { fd = openSync(logp, "a"); writeSync(fd, `\n=== ${name} ${new Date().toISOString()} ===\n$ ${cmd}\n`); } catch {}
584
586
  const entry = { id: randomBytes(4).toString("hex"), name, path: cwd, log: logp, startedAt: Date.now(), status: "running", exitCode: null, endedAt: null };
585
587
  const child = spawn(cmd, { shell: true, cwd, detached: true, stdio: ["ignore", fd === "ignore" ? "ignore" : fd, fd === "ignore" ? "ignore" : fd], ...(env ? { env: { ...process.env, ...env } } : {}) });
@@ -623,7 +625,7 @@ const QUESTIONS_MAX = 5;
623
625
  // the next moves up. The rest are a click away.
624
626
  const OPTIONS_SHOWN = 2, IDEAS_SHOWN = 2;
625
627
  const qKey = (s) => String(s || "").trim().toLowerCase().replace(/\s+/g, " ");
626
- const readSymbiot = (path, f) => { try { return readFileSync(join(path, ".symbiot", f), "utf8"); } catch { return ""; } };
628
+ const readSymbiot = (path, f) => { if (f === "agent.log") return readRunLog(join(path, ".symbiot", f)); try { return readFileSync(join(path, ".symbiot", f), "utf8"); } catch { return ""; } };
627
629
  // "## Questions" → "### question", context lines, "- option" bullets;
628
630
  // "## Suggestions" (or Ideas / Follow-ups) → "- idea" bullets. Forgiving: a bare
629
631
  // bullet under Questions is a question with no options.
@@ -880,8 +882,7 @@ function workOf(path, log) {
880
882
  function agentsList() {
881
883
  const seen = new Set();
882
884
  return HANDOFFS.map((e) => {
883
- let log = ""; try { log = readFileSync(e.log, "utf8"); } catch {}
884
- const { tail, work, progress } = workOf(e.path, log);
885
+ const { tail, work, progress } = workOf(e.path, readRunLog(e.log));
885
886
  const first = !seen.has(e.path); seen.add(e.path);
886
887
  const facts = first && e.status !== "running" ? factsOf(e.path) : [];
887
888
  return { id: e.id, name: e.name, path: e.path, status: e.status, elapsed: (e.endedAt || Date.now()) - e.startedAt, exitCode: e.exitCode, tail, work, progress, changed: agentChanges(e.path, e.startedAt), ask: first ? agentQuestions(e.path, e.name) : null, held: first ? heldTasks(e.path) : null, waiting: first && e.status !== "running" ? waitingFor(e.path) : null, remember: facts.length ? facts : null, fromHeld: !!e.fromHeld };
package/lanes.mjs CHANGED
@@ -23,7 +23,7 @@ import { runHandoff, runningHandoff, waitingFor, agentQuestions } from "./agents
23
23
  import { addTask, pushTasks } from "./tasks.mjs";
24
24
  import { actNow } from "./mind.mjs";
25
25
  import { repoPathMap } from "./scan.mjs";
26
- import { parseRun, lastRunText } from "./work.mjs";
26
+ import { parseRun, lastRunText, readRunLog } from "./work.mjs";
27
27
  import { OPS, parseHandoffs } from "./handover.mjs";
28
28
 
29
29
  const LEDGER = join(CONFIG_DIR, "lanes.json");
@@ -33,7 +33,7 @@ const MAX_KEEP = 200, MAX_CHAIN = 4, TAIL = 4000;
33
33
  function loadLedger() { try { const d = JSON.parse(readFileSync(LEDGER, "utf8")); return { handoffs: Array.isArray(d.handoffs) ? d.handoffs : [] }; } catch { return { handoffs: [] }; } }
34
34
  // It can name accounts and what's in them: yours only (0600).
35
35
  function saveLedger(d) { try { mkdirSync(CONFIG_DIR, { recursive: true }); writeFileSync(LEDGER, JSON.stringify({ handoffs: d.handoffs.slice(-MAX_KEEP) }, null, 1), { mode: 0o600 }); try { chmodSync(LEDGER, 0o600); } catch {} return true; } catch { return false; } }
36
- const readSym = (path, f) => { try { return readFileSync(join(path, ".symbiot", f), "utf8"); } catch { return ""; } };
36
+ const readSym = (path, f) => { if (f === "agent.log") return readRunLog(join(path, ".symbiot", f)); try { return readFileSync(join(path, ".symbiot", f), "utf8"); } catch { return ""; } };
37
37
  const firstLine = (t) => String(t || "").split("\n").find((l) => l.trim()) || "";
38
38
 
39
39
  // A folder's lane: the repo it is, or ops for a run of its own.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "symbiot",
3
- "version": "0.54.0",
3
+ "version": "0.54.2",
4
4
  "description": "Your week, written from your real work. Reads your local git and writes it up with the AI of your choice \u2014 Claude, OpenAI, Gemini, or a local model.",
5
5
  "type": "module",
6
6
  "bin": {
package/server.mjs CHANGED
@@ -7,6 +7,7 @@ import { homedir } from "node:os";
7
7
  import { join } from "node:path";
8
8
  import { fileURLToPath } from "node:url";
9
9
  import { gunzipSync } from "node:zlib";
10
+ import { readRunLog } from "./work.mjs";
10
11
  import { readFileSync, existsSync, mkdirSync, writeFileSync, readdirSync, statSync, unlinkSync } from "node:fs";
11
12
  import { createServer } from "node:http";
12
13
  import { randomBytes } from "node:crypto";
@@ -413,7 +414,7 @@ async function startApp({ bin, since = 7, all = false, c = PLAIN_COLOURS } = {})
413
414
  // giving up and sending you to a terminal.
414
415
  const attempt = (n) => track("symbiot update", inst, homedir(), (code) => {
415
416
  if (code !== 0) {
416
- let tail = ""; try { tail = readFileSync(join(homedir(), ".symbiot", "agent.log"), "utf8").slice(-3000); } catch {}
417
+ const tail = readRunLog(join(homedir(), ".symbiot", "agent.log")).slice(-3000);
417
418
  if (n < 5 && /E404|ETARGET|notarget|No matching version|is not in this registry/i.test(tail)) { UPDATING = { target, retrying: true, attempt: n + 1, at: Date.now() }; setTimeout(() => attempt(n + 1), 30000).unref(); }
418
419
  else UPDATING = null;
419
420
  return;
package/tasks.mjs CHANGED
@@ -11,6 +11,7 @@ import { handoverRules, ONLY_YOU, HANDBACK } from "./handover.mjs";
11
11
  import { userStyleLine } from "./adapt.mjs";
12
12
  import { QUESTIONS_MAX, OPTIONS_SHOWN, IDEAS_SHOWN, handoffCmd, runningHandoff, writeTasks, droppedTasks, startHeldTasks, connectorsLine } from "./agents.mjs";
13
13
  import { gitDefaultBranch } from "./drift.mjs";
14
+ import { readRunLog } from "./work.mjs";
14
15
  import { repoPathMap, openWork, detectRepo } from "./scan.mjs";
15
16
 
16
17
  // ---- tasks: a persistent checklist (stored by core.mjs) -------------------
@@ -342,7 +343,7 @@ function shipWithBump(path, texts, opts) {
342
343
  // What the last run in a folder said when it finished: agent.log after the last
343
344
  // run's "=== name time ===" and "$ command" lines.
344
345
  function runSummary(path) {
345
- let log = ""; try { log = readFileSync(join(path, ".symbiot", "agent.log"), "utf8"); } catch { return ""; }
346
+ const log = readRunLog(join(path, ".symbiot", "agent.log")); if (!log) return "";
346
347
  const at = log.lastIndexOf("\n=== "), rest = at < 0 ? log : log.slice(log.indexOf("\n", at + 1) + 1);
347
348
  return rest.replace(/^\$ .*\n?/, "").trim().slice(-8000);
348
349
  }
package/ui.mjs CHANGED
@@ -1224,7 +1224,7 @@ el.addEventListener('pointerleave',function(){if(!LM.pan)lmHot(null);});
1224
1224
  var zi=$('lmin'),zo=$('lmout'),zf=$('lmfit');function z(f){var w=el.clientWidth/2,h=el.clientHeight/2,k0=LM.view.k,k=Math.max(0.3,Math.min(4,k0*f));LM.view.x=w-(w-LM.view.x)*k/k0;LM.view.y=h-(h-LM.view.y)*k/k0;LM.view.k=k;}
1225
1225
  if(zi)zi.addEventListener('click',function(){z(1.25);});if(zo)zo.addEventListener('click',function(){z(0.8);});if(zf)zf.addEventListener('click',lmFit);}
1226
1226
  function lmStart(){if(LM.on)return;var raf=typeof window.requestAnimationFrame==='function'?function(f){return window.requestAnimationFrame(f);}:null;if(!raf)return;LM.on=true;
1227
- var loop=function(){var p=$('panel-map'),el=$('lmap');if(!p||!el||!el.offsetParent){LM.on=false;return;}lmFrame();raf(loop);};raf(loop);}
1227
+ var loop=function(){var p=$('panel-map'),el=$('lmap');if(!p||!el||!el.offsetParent){LM.on=false;return;}LM.fr=(LM.fr||0)+1;if(LM.fr%glEvery(true)===0)lmFrame();raf(loop);};raf(loop);}
1228
1228
  function lmFrame(){var el=$('lmap'),cv=$('lmc');if(!el||!cv||!LM.items.length)return;if(!LM.G)LM.G=lqGL(cv,64);var w=el.clientWidth,h=el.clientHeight,t=LM.t=(LM.t||0)+0.016,still=LQ.theme&&LQ.theme.still,k=LM.view.k;
1229
1229
  var P=LM.items.map(function(a,i){var p=lmS(a);if(!still){p.x+=Math.sin(t*0.4+i*1.7)*2;p.y+=Math.cos(t*0.35+i*2.3)*2;}return p;}),ME=lmS(LM.me);
1230
1230
  // labels, rings, cluster names, tip
@@ -1235,7 +1235,7 @@ var me=$('lmme');if(me&&me.style)me.style.transform='translate('+Math.round(ME.x
1235
1235
  var tip=$('lmtip');if(tip&&LM.hot){var hi=LM.items.indexOf(LM.hot),hp=P[hi],tw=tip.offsetWidth||280,th=tip.offsetHeight||100,tx=Math.max(10,Math.min(w-tw-10,hp.x+LM.hot.r*k+16)),ty=Math.max(10,Math.min(h-th-10,hp.y-th/2));if(tx<hp.x&&hp.x+LM.hot.r*k+16+tw>w)tx=Math.max(10,hp.x-LM.hot.r*k-16-tw);tip.style.transform='translate('+Math.round(tx)+'px,'+Math.round(ty)+'px)';}
1236
1236
  // faint lines for the weak links
1237
1237
  var sv=$('lmlines');if(sv){var ln='';LM.edges.forEach(function(e){if(e.sim>=0.3)return;var a=P[LM.items.indexOf(e.a)],b=P[LM.items.indexOf(e.b)],hot=LM.hot&&(LM.hot===e.a||LM.hot===e.b);ln+="<line x1='"+a.x.toFixed(1)+"' y1='"+a.y.toFixed(1)+"' x2='"+b.x.toFixed(1)+"' y2='"+b.y.toFixed(1)+"' class='"+(hot?'hot':'')+"'/>";});sv.innerHTML=ln;}
1238
- var G=LM.G;if(!G)return;var gl=G.gl,d=Math.min(window.devicePixelRatio||1,2),W=Math.max(1,Math.floor(w*d)),H=Math.max(1,Math.floor(h*d));if(cv.width!==W||cv.height!==H){cv.width=W;cv.height=H;gl.viewport(0,0,W,H);}
1238
+ var G=LM.G;if(!G)return;var gl=G.gl,d=glDpr(2),W=Math.max(1,Math.floor(w*d)),H=Math.max(1,Math.floor(h*d));if(cv.width!==W||cv.height!==H){cv.width=W;cv.height=H;gl.viewport(0,0,W,H);}
1239
1239
  var o=G.out;for(var z=0;z<o.length;z++)o[z]=0;var n=0,put=function(x,y,r,a){if(n>=64||r<=0)return;o[n*4]=x/w;o[n*4+1]=y/h;o[n*4+2]=r;o[n*4+3]=a||0;n++;};
1240
1240
  LM.items.forEach(function(a,i){if(!a.fold)put(P[i].x,P[i].y,a.r*k*(LM.hot===a?1.08:1),0);});
1241
1241
  // tendrils: a chain of small drops, close enough to always bridge, for the middling links
@@ -1980,13 +1980,13 @@ var x=BS.beads[k],c=BS.b.cards[x.i],n=x.n;if(x.past){tip.innerHTML="<div class='
1980
1980
  tip.innerHTML="<div class='bcard' data-i='"+x.i+"'><div class='bi' data-j='"+x.j+"'><div class='btf'>"+esc(n.name||bsName(c).n)+" <span>&middot; "+esc(bsName(c).n)+" &middot; "+agoTxt(n.ts)+(n.from&&CHATFROM[n.from]?" &middot; "+CHATFROM[n.from]:"")+(n.read?" &middot; read":"")+"</span></div><div class='btx'>"+esc(n.text||'')+"</div><div class='bta'>"+(n.mail||n.chat?"<button class='bdraft'>"+(n.drafted?"Drafted &middot; again":"Draft a reply")+"</button>":"")+(n.href?"<button class='bopen"+(n.mail||n.chat?" quiet":"")+"'>Open</button>":"")+(n.chat&&n.drafted?"<button class='bopenwa quiet'>Open in WhatsApp</button>":"")+"</div></div></div>";
1981
1981
  tip.className='btip on';if(BWIRE)BWIRE(tip);}
1982
1982
  function bsStart(){if(BS.on)return;var raf=typeof window.requestAnimationFrame==='function'?function(f){return window.requestAnimationFrame(f);}:null;if(!raf)return;BS.on=true;
1983
- var loop=function(){var p=$('panel-board'),el=$('bstream');if(!p||!el||!p.offsetParent||(el.classList&&el.classList.contains('none'))){BS.on=false;return;}bsFrame();raf(loop);};raf(loop);}
1983
+ var loop=function(){var p=$('panel-board'),el=$('bstream');if(!p||!el||!p.offsetParent||(el.classList&&el.classList.contains('none'))){BS.on=false;return;}BS.fr=(BS.fr||0)+1;if(BS.fr%glEvery(true)===0)bsFrame();raf(loop);};raf(loop);}
1984
1984
  function bsFrame(){var el=$('bstream'),cv=$('bsc');if(!el||!cv)return;if(!BS.G)BS.G=lqGL(cv,40);var g=bsGeom();if(BS.b&&BS.gw!==g.w){bsBuild(BS.b);g=bsGeom();}var t=LQ.t||0,still=LQ.theme&&LQ.theme.still;
1985
1985
  BS.beads.forEach(function(x,k){var hot=BS.hot===k;x.x+=(x.tx-x.x)*0.12;x.y+=((x.ty+(still?0:Math.sin(t*0.8+k*1.3)*2))-x.y)*0.12;x.r+=((x.tr*(hot?1.35:1))-x.r)*0.15;
1986
1986
  var bt=BS.btns&&BS.btns[k];if(bt&&bt.style){var R=Math.max(14,x.r+6);bt.style.width=bt.style.height=(2*R)+'px';bt.style.transform='translate('+Math.round(x.x-R)+'px,'+Math.round(x.y-R)+'px)';}});
1987
1987
  var tip=document.querySelector('#bsov .btip');if(tip&&BS.hotWait!=null&&BS.waits&&BS.waits[BS.hotWait]){var wx=BS.waits[BS.hotWait],tw2=tip.offsetWidth||300,th2=tip.offsetHeight||140,tx2=Math.max(12,Math.min(g.w-tw2-12,wx.x-tw2/2)),ty2=wx.y-th2-16;if(ty2<8)ty2=wx.y+16;tip.style.transform='translate('+Math.round(tx2)+'px,'+Math.round(ty2)+'px)';}
1988
1988
  else if(tip&&BS.hot!=null&&BS.beads[BS.hot]){var x=BS.beads[BS.hot],tw=tip.offsetWidth||300,th=tip.offsetHeight||120,tx=Math.max(12,Math.min(g.w-tw-12,x.x-tw/2)),ty=x.y-x.r-th-14;if(ty<8)ty=x.y+x.r+14;tip.style.transform='translate('+Math.round(tx)+'px,'+Math.round(ty)+'px)';}
1989
- var G=BS.G;if(!G)return;var gl=G.gl,d=Math.min(window.devicePixelRatio||1,3),W=Math.max(1,Math.floor(g.w*d)),H=Math.max(1,Math.floor(g.h*d));if(cv.width!==W||cv.height!==H){cv.width=W;cv.height=H;gl.viewport(0,0,W,H);}
1989
+ var G=BS.G;if(!G)return;var gl=G.gl,d=glDpr(3),W=Math.max(1,Math.floor(g.w*d)),H=Math.max(1,Math.floor(g.h*d));if(cv.width!==W||cv.height!==H){cv.width=W;cv.height=H;gl.viewport(0,0,W,H);}
1990
1990
  var o=G.out;for(var i=0;i<o.length;i++)o[i]=0;var k=0,put=function(px,py,r,a){if(k>=40)return;o[k*4]=px/g.w;o[k*4+1]=py/g.h;o[k*4+2]=Math.max(0,r);o[k*4+3]=a;k++;};
1991
1991
  (BS.b.cards||[]).forEach(function(c,i){put(g.x1,60+(g.h-110)*(i+0.5)/Math.max(1,g.n),c.count?9+2*Math.min(c.count,6):5,0);});
1992
1992
  BS.beads.forEach(function(x){put(x.x,x.y,x.r,x.need?1:0);});
@@ -2635,7 +2635,17 @@ var LQ_FS=['precision highp float;',
2635
2635
  'gl_FragColor=vec4(col,1.0);}'].join('');
2636
2636
  function lqFS(n){return LQ_FS.split('NB').join(String(n));}
2637
2637
  var LQ_NB=32; // metaballs home can draw: the core, the droplets, the lines to their blobs, the one at your pointer
2638
+ // No graphics card (WebGL drawn by the CPU: SwiftShader, llvmpipe), any steady drawing
2639
+ // keeps several cores spinning (measured: 2.4-6 cores at full speed, 0.9 at 2 a second,
2640
+ // 0.06 not drawing). Then, or when the system asks for less motion, the liquid draws at
2641
+ // a third of the resolution, without the breathing, and only when something moved (Home), at most
2642
+ // 20 a second while you use it and 5 at rest. glDpr: the pixel ratio a canvas draws at;
2643
+ // glEvery: draw 1 frame in n.
2644
+ var GLSOFT=false,GLSTILL=false;try{GLSTILL=!!(window.matchMedia&&window.matchMedia('(prefers-reduced-motion: reduce)').matches);}catch(e){}
2645
+ function glEvery(busy){return GLSOFT||GLSTILL?(busy?3:12):1;}
2646
+ function glDpr(max){return GLSOFT?0.34:Math.min(window.devicePixelRatio||1,max);}
2638
2647
  function lqGL(c,n){c=c||$('lq');n=n||LQ_NB;if(!c||!c.getContext)return null;var gl=null;try{gl=c.getContext('webgl',{antialias:false,alpha:false});}catch(e){}if(!gl)return null;
2648
+ try{var dbg=gl.getExtension('WEBGL_debug_renderer_info'),rn=String(dbg?gl.getParameter(dbg.UNMASKED_RENDERER_WEBGL):gl.getParameter(gl.RENDERER)).toLowerCase();if(['swiftshader','llvmpipe','softpipe','software'].some(function(k){return rn.indexOf(k)>=0;}))GLSOFT=true;}catch(e){}
2639
2649
  function sh(t,src){var o=gl.createShader(t);gl.shaderSource(o,src);gl.compileShader(o);return o;}
2640
2650
  var pr=gl.createProgram();gl.attachShader(pr,sh(gl.VERTEX_SHADER,'attribute vec2 a;void main(){gl_Position=vec4(a,0.0,1.0);}'));gl.attachShader(pr,sh(gl.FRAGMENT_SHADER,lqFS(n)));gl.linkProgram(pr);
2641
2651
  if(!gl.getProgramParameter(pr,gl.LINK_STATUS))return null;gl.useProgram(pr);
@@ -2643,9 +2653,9 @@ var bf=gl.createBuffer();gl.bindBuffer(gl.ARRAY_BUFFER,bf);gl.bufferData(gl.ARRA
2643
2653
  var al=gl.getAttribLocation(pr,'a');gl.enableVertexAttribArray(al);gl.vertexAttribPointer(al,2,gl.FLOAT,false,0,0);
2644
2654
  var U={};['uRes','uT','uDpr','uB','uRip','uStyle','uContrast','uExposure'].forEach(function(n){U[n]=gl.getUniformLocation(pr,n);});
2645
2655
  return {gl:gl,c:c,U:U,out:new Float32Array(n*4),t0:(window.performance&&performance.now)?performance.now():Date.now()};}
2646
- function lqDraw(G){var gl=G.gl,c=G.c,S=lqSize(),d=Math.min(window.devicePixelRatio||1,3),W=Math.max(1,Math.floor(S.w*d)),H=Math.max(1,Math.floor(S.h*d));
2656
+ function lqDraw(G){var gl=G.gl,c=G.c,S=lqSize(),d=glDpr(3),W=Math.max(1,Math.floor(S.w*d)),H=Math.max(1,Math.floor(S.h*d));
2647
2657
  if(c.width!==W||c.height!==H){c.width=W;c.height=H;gl.viewport(0,0,W,H);}
2648
- var now=(window.performance&&performance.now)?performance.now():Date.now(),t=(now-G.t0)/1000,th=LQ.theme,still=th.still,o=G.out;LQ.t=t;
2658
+ var now=(window.performance&&performance.now)?performance.now():Date.now(),t=(now-G.t0)/1000,th=LQ.theme,slow=GLSOFT||GLSTILL,still=th.still||slow,o=G.out;LQ.t=t;
2649
2659
  for(var i=0;i<o.length;i++)o[i]=0;var NB=o.length/4;
2650
2660
  var put=function(k,x,y,r,a,lv){if(k>NB-1)return;var w=still?0:(k===0?1:lv!=null?lv:0.3);o[k*4]=x/S.w+Math.sin(t*1.3+k*1.7)*0.004*w;o[k*4+1]=y/S.h+Math.cos(t*1.1+k*2.3)*0.005*w;o[k*4+2]=Math.max(0,r)*(1+0.035*Math.sin(t*2.0+k)*w);o[k*4+3]=a||0;};
2651
2661
  put(0,LQ.core.x,LQ.core.y,LQ.core.cr);var k=1,tend=[];
@@ -2658,7 +2668,9 @@ tend.forEach(function(dr){var dx=dr.x-LQ.core.x,dy=dr.y-LQ.core.y,len=Math.sqrt(
2658
2668
  if(LQ.mode!=='rest'&&LQ.mode!=='pool')LQ.drops.forEach(function(dr){if(!dr.blob||dr.gone||dr.cr<6||k+3>NB-1)return;var x1=dr.x+dr.cr;put(k++,x1+8,dr.y,7*Math.min(1,dr.cr/dr.r),1,0.2);put(k++,x1+18,dr.y,6,1,0.2);put(k++,x1+31,dr.y,10,1,0.2);});
2659
2669
  var P=LQ.pointer;if(P&&LQ.mode==='aware'&&!LQ.talking){var best=null,bd=1e9;[LQ.core].concat(LQ.drops).forEach(function(g){if(g.cr<18)return;var dd=Math.hypot(P[0]-g.x,P[1]-g.y)-g.cr;if(dd<bd){bd=dd;best=g;}});
2660
2670
  if(best&&bd<230){var dx=P[0]-best.x,dy=P[1]-best.y,dl=Math.hypot(dx,dy)||1,reach=Math.min(dl,best.cr+70);put(NB-1,best.x+dx/dl*reach,best.y+dy/dl*reach,8+20*(1-Math.max(0,bd)/230));}}
2661
- var U=G.U;gl.uniform2f(U.uRes,W,H);gl.uniform1f(U.uT,t);gl.uniform1f(U.uDpr,d);gl.uniform4fv(U.uB,o);gl.uniform3f(U.uRip,LQ.ripple[0],LQ.ripple[1],still?-10:LQ.ripple[2]);
2671
+ // drawn seldom (no GPU, or less motion asked for): still, and only when something moved
2672
+ if(slow){var sig=W+'|'+H+'|'+th.look+th.night+th.contrast;for(var q=0;q<o.length;q++)sig+=','+Math.round(o[q]*400);if(sig===G.sig)return;G.sig=sig;}
2673
+ var U=G.U;gl.uniform2f(U.uRes,W,H);gl.uniform1f(U.uT,slow?0:t);gl.uniform1f(U.uDpr,d);gl.uniform4fv(U.uB,o);gl.uniform3f(U.uRip,LQ.ripple[0],LQ.ripple[1],still?-10:LQ.ripple[2]);
2662
2674
  gl.uniform1f(U.uStyle,th.look==='glass'?0:th.look==='pearl'?2:1);gl.uniform1f(U.uContrast,th.contrast?1:0);gl.uniform1f(U.uExposure,th.night?0.82:1);
2663
2675
  gl.drawArrays(gl.TRIANGLES,0,3);}
2664
2676
  function lqInit(){var bd=document.body;if(!bd||!bd.classList)return;lqTheme();bd.classList.add('lq-liquid');
@@ -2681,7 +2693,7 @@ lqLoad(true);
2681
2693
  // the only clocks: rest when left alone, fresh data now and then, the hour (night)
2682
2694
  setInterval(function(){if(LQ.mode==='aware'&&!LQ.talking&&Date.now()-LQ.lastAct>LQ_REST)LQ.mode='rest';if(LQ.mode!=='pool')lqLoad(false);var h=new Date().getHours(),n=h>=22||h<6;if(n!==LQ.theme.night)lqTheme();},30000);
2683
2695
  var G=lqGL();if(G){try{lqStep();lqDraw(G);}catch(e){}}if(typeof window.requestAnimationFrame!=='function')return;var raf=function(f){return window.requestAnimationFrame(f);};
2684
- function frame(){if(!document.hidden){LQ.frame++;if(LQ.mode!=='pool'||LQ.frame%3===0){lqStep();if(G)lqDraw(G);}}raf(frame);}
2696
+ function frame(){if(!document.hidden){LQ.frame++;if(LQ.mode!=='pool'||LQ.frame%3===0)lqStep();var every=(GLSOFT||GLSTILL)?glEvery(LQ.mode==='aware'):LQ.mode==='pool'?6:LQ.mode==='rest'?2:1;if(G&&LQ.frame%every===0)lqDraw(G);}raf(frame);} // moves every frame; drawn less at rest, behind a panel, without a GPU
2685
2697
  raf(frame);}
2686
2698
 
2687
2699
  initGraphEvents();syncP();refresh();loadMap();firstTab();lqInit();loadWhatsNew();loadAgentCfg();loadScanRoots();loadKnowledge();loadPhone();loadLinks();loadMind();loadTrusted();loadMail();loadScreensUI();loadMonitorsUI();loadDesktop();loadWatchUI();setInterval(function(){whenFree(document.querySelector('main'),'watch',loadWatchUI);},60000);loadPhoneLink();
package/work.mjs CHANGED
@@ -8,6 +8,8 @@
8
8
  // time. Another agent (Codex, Aider, Gemini) writes plain text: its last lines
9
9
  // come through as what it said. Pure: text in, data out.
10
10
 
11
+ import { statSync, openSync, readSync, closeSync } from "node:fs";
12
+
11
13
  const MAX_STEPS = 60;
12
14
  const short = (s, n = 80) => { s = String(s ?? "").replace(/\s+/g, " ").trim(); return s.length > n ? s.slice(0, n - 1) + "…" : s; };
13
15
  const base = (p) => String(p || "").split("/").filter(Boolean).slice(-2).join("/");
@@ -103,6 +105,29 @@ function parseRun(text) {
103
105
  return out;
104
106
  }
105
107
 
108
+ // The newest run of an agent.log, from its "=== " header on, read from the end of the
109
+ // file. A log keeps every run and everything they read (one reached 144 MB: 96 runs and
110
+ // the screenshots they looked at), and every view only needs the newest, so it never
111
+ // reads the rest; and it's kept until the file changes, so a view that asks again
112
+ // costs nothing. Past RUN_CAP of one run, it's that run's last RUN_CAP.
113
+ const RUN_CAP = 16 * 1024 * 1024, RUN_CACHE = new Map();
114
+ function readRunLog(file) {
115
+ let st; try { st = statSync(file); } catch { return ""; }
116
+ const hit = RUN_CACHE.get(file); if (hit && hit.size === st.size && hit.mtime === st.mtimeMs) return hit.text;
117
+ let fd; try { fd = openSync(file, "r"); } catch { return ""; }
118
+ let buf = Buffer.alloc(0), at = -1;
119
+ try {
120
+ for (let end = st.size; end > 0 && buf.length < RUN_CAP;) {
121
+ const start = Math.max(0, end - (1 << 20)), b = Buffer.alloc(end - start);
122
+ readSync(fd, b, 0, b.length, start); buf = Buffer.concat([b, buf]); end = start;
123
+ at = buf.lastIndexOf("\n=== "); if (at >= 0) break;
124
+ }
125
+ } catch { buf = Buffer.alloc(0); } finally { try { closeSync(fd); } catch {} }
126
+ const text = (at >= 0 ? buf.subarray(at) : buf).toString("utf8");
127
+ RUN_CACHE.delete(file); RUN_CACHE.set(file, { size: st.size, mtime: st.mtimeMs, text });
128
+ if (RUN_CACHE.size > 40) RUN_CACHE.delete(RUN_CACHE.keys().next().value);
129
+ return text;
130
+ }
106
131
  // The newest run's part of an agent.log (after its header and command line).
107
132
  function lastRunText(log) {
108
133
  const s = String(log || ""), at = s.lastIndexOf("\n=== ");
@@ -111,4 +136,4 @@ function lastRunText(log) {
111
136
  // What the agent said last: its final answer, from either kind of log.
112
137
  function finalOf(log) { return parseRun(lastRunText(log)).final; }
113
138
 
114
- export { parseRun, lastRunText, finalOf, describe, testsIn };
139
+ export { parseRun, lastRunText, finalOf, describe, testsIn, readRunLog };